From 7ddd98de0a0d2b1b444858f1f6575f927e4c82db Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 3 Apr 2023 20:14:01 +0200 Subject: [PATCH 001/226] Fix warnings in til for an upcoming version of MSVC (#15087) A trivial change. :) --- src/inc/til/color.h | 3 ++- src/inc/til/small_vector.h | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/inc/til/color.h b/src/inc/til/color.h index e90ca88efce..cfeee352288 100644 --- a/src/inc/til/color.h +++ b/src/inc/til/color.h @@ -9,7 +9,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // a number of other color types. #pragma warning(push) // we can't depend on GSL here, so we use static_cast for explicit narrowing -#pragma warning(disable : 26472) +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). +#pragma warning(disable : 26495) // Variable 'til::color::::abgr' is uninitialized. Always initialize a member variable (type.6). struct color { // Clang (10) has no trouble optimizing the COLORREF conversion operator, below, to a diff --git a/src/inc/til/small_vector.h b/src/inc/til/small_vector.h index 127795e131d..4a1fe4eab90 100644 --- a/src/inc/til/small_vector.h +++ b/src/inc/til/small_vector.h @@ -16,6 +16,8 @@ #pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe ... (stl.1). // small_vector::_data references potentially uninitialized data and so we can't pass it regular iterators which reference initialized data. #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +// small_vector::_buffer is explicitly uninitialized, because we manage its initialization manually. +#pragma warning(disable : 26495) // Variable '...' is uninitialized. Always initialize a member variable (type.6). namespace til { From 0d38d17299eed454f81b732ef5cf234cd3c8fba9 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 3 Apr 2023 20:21:22 +0200 Subject: [PATCH 002/226] Add a simple tool to test rendering functionality (#15091) This tool augments `vttest` by adding some things that are specific to us (like non-VT console attributes), and some things `vttest` is seemingly too old for (like emojis). I'm planning to add more "pages" of tests to the application in the future, whenever the need arises. --- .github/actions/spelling/excludes.txt | 1 + OpenConsole.sln | 29 ++ .../RenderingTests/RenderingTests.vcxproj | 26 ++ .../RenderingTests.vcxproj.filters | 4 + src/tools/RenderingTests/main.cpp | 259 ++++++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 src/tools/RenderingTests/RenderingTests.vcxproj create mode 100644 src/tools/RenderingTests/RenderingTests.vcxproj.filters create mode 100644 src/tools/RenderingTests/main.cpp diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index bc509a5669e..3f6e31b1072 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -109,6 +109,7 @@ ^src/tools/integrity/packageuwp/ConsoleUWP\.appxSources$ ^src/tools/lnkd/lnkd\.bat$ ^src/tools/pixels/pixels\.bat$ +^src/tools/RenderingTests/main.cpp$ ^src/tools/texttests/fira\.txt$ ^src/tools/U8U16Test/(?:fr|ru|zh)\.txt$ ^src/types/ut_types/UtilsTests.cpp$ diff --git a/OpenConsole.sln b/OpenConsole.sln index 3116a5e98db..571824e7250 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -418,6 +418,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MidiAudio", "src\audio\midi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TerminalStress", "src\tools\TerminalStress\TerminalStress.csproj", "{613CCB57-5FA9-48EF-80D0-6B1E319E20C4}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RenderingTests", "src\tools\RenderingTests\RenderingTests.vcxproj", "{37C995E0-2349-4154-8E77-4A52C0C7F46D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution AuditMode|Any CPU = AuditMode|Any CPU @@ -2770,6 +2772,32 @@ Global {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|x64.ActiveCfg = Release|Any CPU {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|x64.Build.0 = Release|Any CPU {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|x86.ActiveCfg = Release|Any CPU + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|x64.ActiveCfg = Release|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|x86.ActiveCfg = Release|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM.ActiveCfg = Debug|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM64.Build.0 = Debug|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x64.ActiveCfg = Debug|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x64.Build.0 = Debug|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x86.ActiveCfg = Debug|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x86.Build.0 = Debug|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|Any CPU.ActiveCfg = Release|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM.ActiveCfg = Release|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM64.ActiveCfg = Release|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM64.Build.0 = Release|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x64.ActiveCfg = Release|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x64.Build.0 = Release|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x86.ActiveCfg = Release|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2875,6 +2903,7 @@ Global {40BD8415-DD93-4200-8D82-498DDDC08CC8} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} {3C67784E-1453-49C2-9660-483E2CC7F7AD} = {40BD8415-DD93-4200-8D82-498DDDC08CC8} {613CCB57-5FA9-48EF-80D0-6B1E319E20C4} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {37C995E0-2349-4154-8E77-4A52C0C7F46D} = {A10C4720-DCA4-4640-9749-67F4314F527C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271} diff --git a/src/tools/RenderingTests/RenderingTests.vcxproj b/src/tools/RenderingTests/RenderingTests.vcxproj new file mode 100644 index 00000000000..0824d3dd669 --- /dev/null +++ b/src/tools/RenderingTests/RenderingTests.vcxproj @@ -0,0 +1,26 @@ + + + + 16.0 + Win32Proj + {37c995e0-2349-4154-8e77-4a52c0c7f46d} + RenderingTests + 10.0 + + + + + + NotUsing + _CONSOLE;%(PreprocessorDefinitions) + + + Console + + + + + + + + diff --git a/src/tools/RenderingTests/RenderingTests.vcxproj.filters b/src/tools/RenderingTests/RenderingTests.vcxproj.filters new file mode 100644 index 00000000000..0f14913f3c7 --- /dev/null +++ b/src/tools/RenderingTests/RenderingTests.vcxproj.filters @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/tools/RenderingTests/main.cpp b/src/tools/RenderingTests/main.cpp new file mode 100644 index 00000000000..49cac079889 --- /dev/null +++ b/src/tools/RenderingTests/main.cpp @@ -0,0 +1,259 @@ +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include + +// Another variant of "defer" for C++. +namespace +{ + namespace detail + { + + template + class scope_guard + { + public: + scope_guard(F f) noexcept : + func(std::move(f)) + { + } + + ~scope_guard() + { + func(); + } + + scope_guard(const scope_guard&) = delete; + scope_guard(scope_guard&& rhs) = delete; + scope_guard& operator=(const scope_guard&) = delete; + scope_guard& operator=(scope_guard&&) = delete; + + private: + F func; + }; + + enum class scope_guard_helper + { + }; + + template + scope_guard operator+(scope_guard_helper /*unused*/, F&& fn) + { + return scope_guard(std::forward(fn)); + } + + } // namespace detail + +// The extra indirection is necessary to prevent __LINE__ to be treated literally. +#define _DEFER_CONCAT_IMPL(a, b) a##b +#define _DEFER_CONCAT(a, b) _DEFER_CONCAT_IMPL(a, b) +#define defer const auto _DEFER_CONCAT(_defer_, __LINE__) = ::detail::scope_guard_helper() + [&]() +} + +static void printUTF16(const wchar_t* str) +{ + WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), str, static_cast(wcslen(str)), nullptr, nullptr); +} + +// wprintf() in the uCRT prints every single wchar_t individually and thus breaks surrogate +// pairs apart which Windows Terminal treats as invalid input and replaces it with U+FFFD. +static void printfUTF16(_In_z_ _Printf_format_string_ wchar_t const* const format, ...) +{ + std::array buffer; + + va_list args; + va_start(args, format); + const auto length = _vsnwprintf_s(buffer.data(), buffer.size(), _TRUNCATE, format, args); + va_end(args); + + assert(length >= 0); + WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), buffer.data(), length, nullptr, nullptr); +} + +static void wait() +{ + printUTF16(L"\x1B[9999;1HPress any key to continue..."); + _getch(); +} + +static void clear() +{ + printUTF16( + L"\x1B[H" // move cursor to 0,0 + L"\x1B[2J" // clear screen + ); +} + +int main() +{ + const auto outputHandle = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD consoleMode = ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT; + GetConsoleMode(outputHandle, &consoleMode); + SetConsoleMode(outputHandle, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + defer + { + SetConsoleMode(outputHandle, consoleMode); + }; + + printUTF16( + L"\x1b[?1049h" // enable alternative screen buffer + ); + defer + { + printUTF16( + L"\x1b[?1049l" // disable alternative screen buffer + ); + }; + + { + struct ConsoleAttributeTest + { + const wchar_t* text = nullptr; + WORD attribute = 0; + }; + static constexpr ConsoleAttributeTest consoleAttributeTests[]{ + { L"Console attributes:", 0 }, +#define MAKE_TEST_FOR_ATTRIBUTE(attr) { L## #attr, attr } + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_GRID_HORIZONTAL), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_GRID_LVERTICAL), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_GRID_RVERTICAL), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_REVERSE_VIDEO), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_UNDERSCORE), +#undef MAKE_TEST_FOR_ATTRIBUTE + { L"all gridlines", COMMON_LVB_GRID_HORIZONTAL | COMMON_LVB_GRID_LVERTICAL | COMMON_LVB_GRID_RVERTICAL | COMMON_LVB_UNDERSCORE }, + { L"all attributes", COMMON_LVB_GRID_HORIZONTAL | COMMON_LVB_GRID_LVERTICAL | COMMON_LVB_GRID_RVERTICAL | COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE }, + }; + + SHORT row = 2; + for (const auto& t : consoleAttributeTests) + { + const auto length = static_cast(wcslen(t.text)); + printfUTF16(L"\x1B[%d;5H%s", row + 1, t.text); + + WORD attributes[32]; + std::fill_n(&attributes[0], length, static_cast(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | t.attribute)); + + DWORD numberOfAttrsWritten; + WriteConsoleOutputAttribute(outputHandle, attributes, length, { 4, row }, &numberOfAttrsWritten); + + row += 2; + } + + struct VTAttributeTest + { + const wchar_t* text = nullptr; + int sgr = 0; + }; + static constexpr VTAttributeTest vtAttributeTests[]{ + { L"ANSI escape SGR:", 0 }, + { L"italic", 3 }, + { L"underline", 4 }, + { L"reverse", 7 }, + { L"strikethrough", 9 }, + { L"double underline", 21 }, + { L"overlined", 53 }, + }; + + row = 3; + for (const auto& t : vtAttributeTests) + { + printfUTF16(L"\x1B[%d;45H\x1b[%dm%s\x1b[m", row, t.sgr, t.text); + row += 2; + } + + printfUTF16(L"\x1B[%d;45H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); + + wait(); + clear(); + } + + { + printUTF16( + L"\x1B[3;5HDECDWL Double Width \U0001FAE0 A\u0353\u0353 B\u036F\u036F" + L"\x1B[4;5H\x1b#6DECDWL Double Width \U0001FAE0 A\u0353\u0353 B\u036F\u036F" + L"\x1B[8;5HDECDHL Double Height \U0001F642\U0001F6C1 A\u0353\u0353 B\u036F\u036F X\u0353\u0353 Y\u036F\u036F" + L"\x1B[9;5H\x1b#3DECDHL Double Height Top \U0001F642 A\u0353\u0353 B\u036F\u036F" + L"\x1B[10;5H\x1b#4DECDHL Double Height Bottom \U0001F6C1 X\u0353\u0353 Y\u036F\u036F"); + + wait(); + clear(); + } + + { + defer + { + // Setting an empty DRCS gets us back to the regular font. + printUTF16(L"\x1bP1;1;2{ @\x1b\\"); + }; + + constexpr auto width = 14; + const auto glyph = + "W W " + "W W " + "W W W " + "W W W " + "W W W " + "W W W TTTTTTT" + " W W T " + " T " + " T " + " T " + " T " + " T "; + + // Convert the above visual glyph to sixels + wchar_t rows[2][width]; + for (int r = 0; r < 2; ++r) + { + const auto glyphData = &glyph[r * width * 6]; + + for (int x = 0; x < width; ++x) + { + unsigned int accumulator = 0; + for (int y = 5; y >= 0; --y) + { + const auto isSet = glyphData[y * width + x] != ' '; + accumulator <<= 1; + accumulator |= static_cast(isSet); + } + + rows[r][x] = static_cast(L'?' + accumulator); + } + } + + // DECDLD - Dynamically Redefinable Character Sets + printfUTF16( + // * Pfn | font number | 1 | + // * Pcn | starting character | 3 | = ASCII 0x23 "#" + // * Pe | erase control | 2 | erase all + // Pcmw | character matrix width | %d | `width` pixels + // Pw | font width | 0 | 80 columns + // Pt | text or full cell | 0 | text + // Pcmh | character matrix height | 0 | 12 pixels + // Pcss | character set size | 0 | 94 + // * Dscs | character set name | " @" | unregistered soft set + L"\x1bP1;3;2;%d{ @%.15s/%.15s\x1b\\", + width, + rows[0], + rows[1]); + +#define DRCS_SEQUENCE L"\x1b( @#\x1b(A" + printUTF16( + L"\x1B[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" + L"\x1B[5;5HRegular: " DRCS_SEQUENCE L"" + L"\x1B[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" + L"\x1B[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" + L"\x1B[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" + // We map soft fonts into the private use area starting at U+EF20. This test ensures + // that we correctly map actual fallback glyphs mixed into the DRCS glyphs. + L"\x1B[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); +#undef DRCS_SEQUENCE + + wait(); + } + + return 0; +} From 0656afcf1313ec38afcc3dfe5d1dca3754fe9aae Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 3 Apr 2023 20:39:36 +0200 Subject: [PATCH 003/226] Expose hyperlink attributes in PaintBufferGridLines (#15090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendering hyperlinks is unneccessarily complex at the moment, because it requires you to implement `UpdateDrawingBrushes`, manually extract the hyperlink flag from the given `TextAttribute` and save it until the next call to `PaintBufferGridLines` which does not get that flag. This isn't particularly clean as it assumes that `PaintBufferGridLines` will be called after `UpdateDrawingBrushes` in the first place. Instead, we can simply pass the hyperlink flag to `UpdateDrawingBrushes` so that the renderers don't need to deal with this anymore. ## PR Checklist * Hyperlinks show up with a dotted line ✅ * Hovering hyperlinks underline them ✅ --- src/cascadia/TerminalControl/ControlCore.cpp | 2 +- src/renderer/base/renderer.cpp | 58 ++++++++++++-------- src/renderer/base/renderer.hpp | 6 +- src/renderer/dx/DxRenderer.cpp | 8 +-- src/renderer/dx/DxRenderer.hpp | 1 - src/renderer/inc/IRenderEngine.hpp | 28 +++++----- 6 files changed, 56 insertions(+), 47 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 54cca0ca559..80205d6045a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -723,7 +723,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _lastHoveredId = newId; _lastHoveredInterval = newInterval; - _renderEngine->UpdateHyperlinkHoveredId(newId); + _renderer->UpdateHyperlinkHoveredId(newId); _renderer->UpdateLastHoveredInterval(newInterval); _renderer->TriggerRedrawAll(); } diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index f3b9517d899..fe2bbded353 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -913,55 +913,55 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, } // Method Description: -// - Generates a IRenderEngine::GridLines structure from the values in the +// - Generates a GridLines structure from the values in the // provided textAttribute // Arguments: // - textAttribute: the TextAttribute to generate GridLines from. // Return Value: // - a GridLineSet containing all the gridline info from the TextAttribute -IRenderEngine::GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept +GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept { // Convert console grid line representations into rendering engine enum representations. - IRenderEngine::GridLineSet lines; + GridLineSet lines; if (textAttribute.IsTopHorizontalDisplayed()) { - lines.set(IRenderEngine::GridLines::Top); + lines.set(GridLines::Top); } if (textAttribute.IsBottomHorizontalDisplayed()) { - lines.set(IRenderEngine::GridLines::Bottom); + lines.set(GridLines::Bottom); } if (textAttribute.IsLeftVerticalDisplayed()) { - lines.set(IRenderEngine::GridLines::Left); + lines.set(GridLines::Left); } if (textAttribute.IsRightVerticalDisplayed()) { - lines.set(IRenderEngine::GridLines::Right); + lines.set(GridLines::Right); } if (textAttribute.IsCrossedOut()) { - lines.set(IRenderEngine::GridLines::Strikethrough); + lines.set(GridLines::Strikethrough); } if (textAttribute.IsUnderlined()) { - lines.set(IRenderEngine::GridLines::Underline); + lines.set(GridLines::Underline); } if (textAttribute.IsDoublyUnderlined()) { - lines.set(IRenderEngine::GridLines::DoubleUnderline); + lines.set(GridLines::DoubleUnderline); } if (textAttribute.IsHyperlink()) { - lines.set(IRenderEngine::GridLines::HyperlinkUnderline); + lines.set(GridLines::HyperlinkUnderline); } return lines; } @@ -985,19 +985,10 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin auto lines = Renderer::s_GetGridlines(textAttribute); // For now, we dash underline patterns and switch to regular underline on hover - // Since we're only rendering pattern links on *hover*, there's no point in checking - // the pattern range if we aren't currently hovering. - if (_hoveredInterval.has_value()) + if (_isHoveredHyperlink(textAttribute) || _isInHoveredInterval(coordTarget)) { - const til::point coordTargetTil{ coordTarget }; - if (_hoveredInterval->start <= coordTargetTil && - coordTargetTil <= _hoveredInterval->stop) - { - if (_pData->GetPatternId(coordTarget).size() > 0) - { - lines.set(IRenderEngine::GridLines::Underline); - } - } + lines.reset(GridLines::HyperlinkUnderline); + lines.set(GridLines::Underline); } // Return early if there are no lines to paint. @@ -1010,6 +1001,18 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin } } +bool Renderer::_isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept +{ + return _hyperlinkHoveredId && _hyperlinkHoveredId == textAttribute.GetHyperlinkId(); +} + +bool Renderer::_isInHoveredInterval(const til::point coordTarget) const noexcept +{ + return _hoveredInterval && + _hoveredInterval->start <= coordTarget && coordTarget <= _hoveredInterval->stop && + _pData->GetPatternId(coordTarget).size() > 0; +} + // Routine Description: // - Retrieve information about the cursor, and pack it into a CursorOptions // which the render engine can use for painting the cursor. @@ -1362,6 +1365,15 @@ void Renderer::ResetErrorStateAndResume() EnablePainting(); } +void Renderer::UpdateHyperlinkHoveredId(uint16_t id) noexcept +{ + _hyperlinkHoveredId = id; + FOREACH_ENGINE(pEngine) + { + pEngine->UpdateHyperlinkHoveredId(id); + } +} + void Renderer::UpdateLastHoveredInterval(const std::optional& newInterval) { _hoveredInterval = newInterval; diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index e41101f7bf5..3a10a255aff 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -89,10 +89,11 @@ namespace Microsoft::Console::Render void SetRendererEnteredErrorStateCallback(std::function pfn); void ResetErrorStateAndResume(); + void UpdateHyperlinkHoveredId(uint16_t id) noexcept; void UpdateLastHoveredInterval(const std::optional::interval>& newInterval); private: - static IRenderEngine::GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept; + static GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept; static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); [[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept; @@ -101,6 +102,7 @@ namespace Microsoft::Console::Render void _PaintBufferOutput(_In_ IRenderEngine* const pEngine); void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target, const bool lineWrapped); void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, const TextAttribute textAttribute, const size_t cchLine, const til::point coordTarget); + bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept; void _PaintSelection(_In_ IRenderEngine* const pEngine); void _PaintCursor(_In_ IRenderEngine* const pEngine); void _PaintOverlays(_In_ IRenderEngine* const pEngine); @@ -110,6 +112,7 @@ namespace Microsoft::Console::Render std::vector _GetSelectionRects() const; void _ScrollPreviousSelection(const til::point delta); [[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine); + bool _isInHoveredInterval(til::point coordTarget) const noexcept; [[nodiscard]] std::optional _GetCursorInfo(); [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); @@ -119,6 +122,7 @@ namespace Microsoft::Console::Render std::unique_ptr _pThread; static constexpr size_t _firstSoftFontChar = 0xEF20; size_t _lastSoftFontChar = 0; + uint16_t _hyperlinkHoveredId = 0; std::optional::interval> _hoveredInterval; Microsoft::Console::Types::Viewport _viewport; std::vector _clusterBuffer; diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 70e6e01806e..f74bf528dc1 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -821,7 +821,6 @@ static constexpr D2D1_ALPHA_MODE _dxgiAlphaToD2d1Alpha(DXGI_ALPHA_MODE mode) noe // 1234123412341234 static constexpr std::array hyperlinkDashes{ 1.f, 3.f }; RETURN_IF_FAILED(_d2dFactory->CreateStrokeStyle(&_dashStrokeStyleProperties, hyperlinkDashes.data(), gsl::narrow_cast(hyperlinkDashes.size()), &_dashStrokeStyle)); - _hyperlinkStrokeStyle = _dashStrokeStyle; // If in composition mode, apply scaling factor matrix if (_chainMode == SwapChainMode::ForComposition) @@ -1723,7 +1722,7 @@ try }; const auto DrawHyperlinkLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { - _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _hyperlinkStrokeStyle.Get()); + _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _dashStrokeStyle.Get()); }; // NOTE: Line coordinates are centered within the line, so they need to be @@ -1980,11 +1979,6 @@ try _drawingContext->useItalicFont = textAttributes.IsItalic(); } - if (textAttributes.IsHyperlink()) - { - _hyperlinkStrokeStyle = (textAttributes.GetHyperlinkId() == _hyperlinkHoveredId) ? _strokeStyle : _dashStrokeStyle; - } - // Update pixel shader settings as background color might have changed _ComputePixelShaderSettings(); diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 9dc9fdff741..bfb11205a05 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -203,7 +203,6 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _customRenderer; ::Microsoft::WRL::ComPtr _strokeStyle; ::Microsoft::WRL::ComPtr _dashStrokeStyle; - ::Microsoft::WRL::ComPtr _hyperlinkStrokeStyle; std::unique_ptr _fontRenderData; DxSoftFont _softFont; diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 05d6b33eed1..46221ae911d 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -32,23 +32,23 @@ namespace Microsoft::Console::Render std::optional cursorInfo; }; + enum class GridLines + { + None, + Top, + Bottom, + Left, + Right, + Underline, + DoubleUnderline, + Strikethrough, + HyperlinkUnderline + }; + using GridLineSet = til::enumset; + class __declspec(novtable) IRenderEngine { public: - enum class GridLines - { - None, - Top, - Bottom, - Left, - Right, - Underline, - DoubleUnderline, - Strikethrough, - HyperlinkUnderline - }; - using GridLineSet = til::enumset; - #pragma warning(suppress : 26432) // If you define or delete any default operation in the type '...', define or delete them all (c.21). virtual ~IRenderEngine() = default; From 06174a9cb3a87f40851a1a733d8a87f189858156 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 3 Apr 2023 16:22:25 -0500 Subject: [PATCH 004/226] Format URLs for display when we show the tooltip (#15095) This will reduce the incidence of confusables, RTL, and non-printables messing with the display of the URL. --- .github/actions/spelling/expect/expect.txt | 1 + src/cascadia/TerminalControl/TermControl.cpp | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 6eda39c7185..e22a3c676b5 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -257,6 +257,7 @@ condrv conechokey conemu configurability +confusables conhost conime conimeinfo diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index d772c6f1ecc..5937f4436e9 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3016,7 +3016,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto lastHoveredCell = _core.HoveredCell(); if (lastHoveredCell) { - const auto uriText = _core.HoveredUriText(); + winrt::hstring uriText = _core.HoveredUriText(); + try + { + // DisplayUri will filter out non-printable characters and confusables. + Windows::Foundation::Uri parsedUri{ uriText }; + uriText = parsedUri.DisplayUri(); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + uriText = {}; + } + if (!uriText.empty()) { const auto panel = SwapChainPanel(); From 17cf44fa71defcdf8f7a03cce0a8d848c8cd8aa3 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 3 Apr 2023 17:40:46 -0500 Subject: [PATCH 005/226] Upgrade to MUX 2.8 (#15078) Updates the Terminal to Microsoft.UI.Xaml v2.8. * MUX 2.8 adds a dependency on WebView2, so we need to include parts of it too. * See https://github.com/microsoft/microsoft-ui-xaml/pull/7574 for why we're adding the `.props` * The TabView thing: > tl;dr: In >=MUX 2.7, we were updating our tab colors by doing a "Visual State Dance", as I called it. We'd manually change the `TabViewItem`'s VisualState to one that it wasn't in, then change it back to the one it should be in. This seemingly re-applied the new values of the brushes. However in 2.8, this seemingly didn't work anymore! > > So instead, we do a "Theme Dance", like so: > ```c++ > const auto& reqTheme = TabViewItem().RequestedTheme(); > TabViewItem().RequestedTheme(ElementTheme::Light); > TabViewItem().RequestedTheme(ElementTheme::Dark); > TabViewItem().RequestedTheme(reqTheme); > ``` > This causes the `ThemeResource`s to be re-evaluated to the new values. > We never got to the root cause of why this seems different in 2.8. It literally makes no sense. Closes #13495 Co-authored-by: Dustin L. Howett --- .../Microsoft.UI.Xaml.Additional.targets | 16 +++++ dep/nuget/packages.config | 3 +- src/cascadia/TerminalApp/TabBase.cpp | 71 ++++++++++--------- src/common.nugetversions.props | 3 + src/common.nugetversions.targets | 13 +++- 5 files changed, 68 insertions(+), 38 deletions(-) create mode 100644 build/rules/Microsoft.UI.Xaml.Additional.targets diff --git a/build/rules/Microsoft.UI.Xaml.Additional.targets b/build/rules/Microsoft.UI.Xaml.Additional.targets new file mode 100644 index 00000000000..dbd6eb3f35c --- /dev/null +++ b/build/rules/Microsoft.UI.Xaml.Additional.targets @@ -0,0 +1,16 @@ + + + + + + + diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index 5015d1ee970..09a9c4935d4 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -8,7 +8,8 @@ - + + diff --git a/src/cascadia/TerminalApp/TabBase.cpp b/src/cascadia/TerminalApp/TabBase.cpp index 54b25c48b08..6761ab3dac1 100644 --- a/src/cascadia/TerminalApp/TabBase.cpp +++ b/src/cascadia/TerminalApp/TabBase.cpp @@ -475,33 +475,36 @@ namespace winrt::TerminalApp::implementation // In GH#11294 we thought we'd still need to set // TabViewItemHeaderBackground manually, but GH#11382 discovered that // Background() was actually okay after all. + + const auto& tabItemResources{ TabViewItem().Resources() }; + TabViewItem().Background(deselectedTabBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), selectedTabBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPointerOver"), hoverTabBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPressed"), selectedTabBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), selectedTabBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPointerOver"), hoverTabBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPressed"), selectedTabBrush); // Similarly, TabViewItem().Foreground() sets the color for the text // when the TabViewItem isn't selected, but not when it is hovered, // pressed, dragged, or selected, so we'll need to just set them all // anyways. TabViewItem().Foreground(deselectedFontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForeground"), deselectedFontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundSelected"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPointerOver"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPressed"), fontBrush); - - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForeground"), deselectedFontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPressed"), secondaryFontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPointerOver"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderPressedCloseButtonForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderPointerOverCloseButtonForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderSelectedCloseButtonForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPressed"), subtleFillColorTertiaryBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPointerOver"), subtleFillColorSecondaryBrush); - - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundActiveTab"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundPressed"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundPointerOver"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderForeground"), deselectedFontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderForegroundSelected"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderForegroundPointerOver"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderForegroundPressed"), fontBrush); + + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForeground"), deselectedFontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPressed"), secondaryFontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPointerOver"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderPressedCloseButtonForeground"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderPointerOverCloseButtonForeground"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderSelectedCloseButtonForeground"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPressed"), subtleFillColorTertiaryBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPointerOver"), subtleFillColorSecondaryBrush); + + tabItemResources.Insert(winrt::box_value(L"TabViewButtonForegroundActiveTab"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewButtonForegroundPressed"), fontBrush); + tabItemResources.Insert(winrt::box_value(L"TabViewButtonForegroundPointerOver"), fontBrush); _RefreshVisualState(); } @@ -537,13 +540,15 @@ namespace winrt::TerminalApp::implementation L"TabViewButtonForegroundPointerOver" }; + const auto& tabItemResources{ TabViewItem().Resources() }; + // simply clear any of the colors in the tab's dict for (const auto& keyString : keys) { auto key = winrt::box_value(keyString); - if (TabViewItem().Resources().HasKey(key)) + if (tabItemResources.HasKey(key)) { - TabViewItem().Resources().Remove(key); + tabItemResources.Remove(key); } } @@ -556,24 +561,20 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // Toggles the visual state of the tab view item, - // so that changes to the tab color are reflected immediately + // BODGY + // - Toggles the requested theme of the tab view item, + // so that changes to the tab color are reflected immediately + // - Prior to MUX 2.8, we toggled the visual state here, but that seemingly + // doesn't work in 2.8. // Arguments: // - // Return Value: // - void TabBase::_RefreshVisualState() { - if (TabViewItem().IsSelected()) - { - VisualStateManager::GoToState(TabViewItem(), L"Normal", true); - VisualStateManager::GoToState(TabViewItem(), L"Selected", true); - } - else - { - VisualStateManager::GoToState(TabViewItem(), L"Selected", true); - VisualStateManager::GoToState(TabViewItem(), L"Normal", true); - } + const auto& reqTheme = TabViewItem().RequestedTheme(); + TabViewItem().RequestedTheme(ElementTheme::Light); + TabViewItem().RequestedTheme(ElementTheme::Dark); + TabViewItem().RequestedTheme(reqTheme); } - } diff --git a/src/common.nugetversions.props b/src/common.nugetversions.props index 19147f5e977..a3ac0f8ec9b 100644 --- a/src/common.nugetversions.props +++ b/src/common.nugetversions.props @@ -36,4 +36,7 @@ $(MSBuildThisFileDirectory)..\packages\Selenium.Support.3.5.0 + + + diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index 91a26fdf59a..e047b9931d2 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -16,6 +16,8 @@ "$(SolutionDir)dep\nuget\nuget.exe" $(SolutionDir)dep\nuget\packages.config + + $(MSBuildThisFileDirectory)..\packages\Microsoft.Web.WebView2.1.0.1661.34 - + + + @@ -86,7 +94,8 @@ - + + From 5de1fd9a7bd0091112fc5ac8287430e4b12d9fa7 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 4 Apr 2023 15:47:36 +0200 Subject: [PATCH 006/226] Introduce til::generational - a struct comparison helper (#15088) It can be costly, difficult, or often impossible to compare two instances of a struct. This little helper can simplify this. The underlying idea is that changes in state occur much less often than the amount of data that's being processed in between. As such, this helper assumes that _any_ modification to the struct it wraps is a state change. When you compare the modified instance with another the comparison operator will then always return false. This makes state changes potentially more costly, because more state might be invalidated than was necessary, but on the other hand it makes both, the code simpler and the fast-path (no state change) much faster. For instance, let's look at the amount of data that represents a user's chosen font: It encompasses the font family, size and weight, font axes (a vector of tuples), dpi and cell height/width overrides. Comparing all that data, every time the user changes anything, is fairly complex to code and maintain and costly at runtime, even though the user will change the only font very seldomly. Instead, we can optimize for the common case of no font changes occuring and simply assume that if any font related field changed, all fields changed. This is exactly what `til::generational` does. --- .github/actions/spelling/patterns/0_t.txt | 2 +- src/inc/til/generational.h | 69 +++++++++++++++++++ src/til/ut_til/GenerationalTests.cpp | 42 +++++++++++ src/til/ut_til/til.unit.tests.vcxproj | 6 +- src/til/ut_til/til.unit.tests.vcxproj.filters | 12 +++- tools/ConsoleTypes.natvis | 7 ++ 6 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/inc/til/generational.h create mode 100644 src/til/ut_til/GenerationalTests.cpp diff --git a/.github/actions/spelling/patterns/0_t.txt b/.github/actions/spelling/patterns/0_t.txt index 0e44139a32e..519c4ec0678 100644 --- a/.github/actions/spelling/patterns/0_t.txt +++ b/.github/actions/spelling/patterns/0_t.txt @@ -10,4 +10,4 @@ \\tests(?![a-z]) \\thread(?![a-z]) \\tools(?![a-z]) -\\types(?![a-z]) +\\types?(?![a-z]) diff --git a/src/inc/til/generational.h b/src/inc/til/generational.h new file mode 100644 index 00000000000..261fbc392bc --- /dev/null +++ b/src/inc/til/generational.h @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + struct generation_t + { + auto operator<=>(const generation_t&) const = default; + + constexpr void bump() noexcept + { + _value++; + } + + uint32_t _value = 0; + }; + + // It can be costly, difficult, or often impossible to compare two + // instances of a struct. This little helper can simplify this. + // + // The underlying idea is that changes in state occur much less often than + // the amount of data that's being processed in between. As such, this + // helper assumes that _any_ modification to the struct it wraps is a + // state change. When you compare the modified instance with another + // the comparison operator will then always return false. This makes + // state changes potentially more costly, because more state might be + // invalidated than was necessary, but on the other hand it makes both, + // the code simpler and the fast-path (no state change) much faster. + template + struct generational + { + generational() = default; + explicit constexpr generational(auto&&... args) : + _value{ std::forward(args)... } {} + explicit constexpr generational(generation_t generation, auto&&... args) : + _generation{ generation }, + _value{ std::forward(args)... } {} + + constexpr bool operator==(const generational& rhs) const noexcept { return generation() == rhs.generation(); } + constexpr bool operator!=(const generational& rhs) const noexcept { return generation() != rhs.generation(); } + + constexpr generation_t generation() const noexcept + { + return _generation; + } + + [[nodiscard]] constexpr const T* operator->() const noexcept + { + return &_value; + } + + [[nodiscard]] constexpr const T& operator*() const noexcept + { + return _value; + } + + [[nodiscard]] constexpr T* write() noexcept + { + _generation.bump(); + return &_value; + } + + private: + generation_t _generation; + T _value{}; + }; +} diff --git a/src/til/ut_til/GenerationalTests.cpp b/src/til/ut_til/GenerationalTests.cpp new file mode 100644 index 00000000000..ce8e4dc1cd3 --- /dev/null +++ b/src/til/ut_til/GenerationalTests.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +struct Data +{ + int value = 0; +}; + +class GenerationalTests +{ + TEST_CLASS(GenerationalTests); + + TEST_METHOD(Basic) + { + til::generational src; + til::generational dst; + + // Reads via -> and *, just like std::optional, etc. + VERIFY_ARE_EQUAL(0, src->value); + VERIFY_ARE_EQUAL(0, (*src).value); + + // Writes via .write()-> + src.write()->value = 123; + // ...which makes them not compare as equal. + VERIFY_ARE_NOT_EQUAL(dst, src); + + // Synchronize the two objects by copying them + dst = src; + // ...which results in both being considered equal again + VERIFY_ARE_EQUAL(dst, src); + // ...and all values are copied over. + VERIFY_ARE_EQUAL(123, dst->value); + } +}; diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index 5223074ec1d..824bf40bd4d 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -20,6 +20,7 @@ + @@ -43,10 +44,12 @@ + + @@ -66,6 +69,7 @@ + @@ -85,4 +89,4 @@ - + \ No newline at end of file diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index 88fccdbd38a..545bc412f55 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -28,6 +28,7 @@ + @@ -118,10 +119,19 @@ inc + + inc + + + inc + + + inc + {7cf29ba4-d33d-4c3b-82e3-ab73e5a79685} - + \ No newline at end of file diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis index bf772d21916..803bf5d3337 100644 --- a/tools/ConsoleTypes.natvis +++ b/tools/ConsoleTypes.natvis @@ -105,4 +105,11 @@ _ptr + + + {{ gen={_generation._value}, {_value} }} + + _value + + From c0f14567f349a44b732221662beb3a62ca49e1f4 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 4 Apr 2023 09:21:43 -0500 Subject: [PATCH 007/226] Use DIPs for the window bounds when tearing out (#15094) Fixes a bug where you'd drag across the boundary and the new window would be at the wrong size related to #14957 --- src/cascadia/TerminalApp/TerminalWindow.cpp | 8 ++++---- src/cascadia/WindowsTerminal/AppHost.cpp | 12 ++++++------ src/inc/til/size.h | 4 ++-- src/til/ut_til/SizeTests.cpp | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index dd984c6c126..07cc41274ee 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -613,11 +613,11 @@ namespace winrt::TerminalApp::implementation if (_contentBounds) { // If we've been created as a torn-out window, then we'll need to - // use that size instead. _contentBounds is in raw pixels. Huzzah! - // Just return that. + // use that size instead. _contentBounds is in DIPs. Scale + // accordingly to the new pixel size. return { - _contentBounds.Value().Width, - _contentBounds.Value().Height + _contentBounds.Value().Width * scale, + _contentBounds.Value().Height * scale }; } diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 3776017f25e..df55bfa7e0f 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -1231,7 +1231,6 @@ winrt::TerminalApp::TerminalWindow AppHost::Logic() void AppHost::_handleMoveContent(const winrt::Windows::Foundation::IInspectable& /*sender*/, winrt::TerminalApp::RequestMoveContentArgs args) { - winrt::Windows::Foundation::Rect rect{}; winrt::Windows::Foundation::IReference windowBoundsReference{ nullptr }; if (args.WindowPosition() && _window) @@ -1280,12 +1279,13 @@ void AppHost::_handleMoveContent(const winrt::Windows::Foundation::IInspectable& dragPositionInPixels.y -= nonClientFrame.top; windowSize = windowSize - nonClientFrame.size(); + // Convert to DIPs for the size, so that dragging across a DPI boundary + // retains the correct dimensions. + const auto sizeInDips = windowSize.scale(til::math::rounding, 1.0f / scale); + til::rect inDips{ dragPositionInPixels, sizeInDips }; + // Use the drag event as the new position, and the size of the actual window. - rect = winrt::Windows::Foundation::Rect{ static_cast(dragPositionInPixels.x), - static_cast(dragPositionInPixels.y), - static_cast(windowSize.width), - static_cast(windowSize.height) }; - windowBoundsReference = rect; + windowBoundsReference = inDips.to_winrt_rect(); } _windowManager.RequestMoveContent(args.Window(), args.Content(), args.TabIndex(), windowBoundsReference); diff --git a/src/inc/til/size.h b/src/inc/til/size.h index cf91a08a0d7..72dbb602852 100644 --- a/src/inc/til/size.h +++ b/src/inc/til/size.h @@ -75,7 +75,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } template>> - constexpr size scale(TilMath math, const T scale) const + [[nodiscard]] constexpr size scale(TilMath math, const T scale) const { return { math, @@ -84,7 +84,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" }; } - constexpr size divide_ceil(const size other) const + [[nodiscard]] constexpr size divide_ceil(const size other) const { // The integer ceil division `((a - 1) / b) + 1` only works for numbers >0. // Support for negative numbers wasn't deemed useful at this point. diff --git a/src/til/ut_til/SizeTests.cpp b/src/til/ut_til/SizeTests.cpp index 357ab300785..a75b48922a2 100644 --- a/src/til/ut_til/SizeTests.cpp +++ b/src/til/ut_til/SizeTests.cpp @@ -306,7 +306,7 @@ class SizeTests constexpr auto scale = 1e12f; auto fn = [&]() { - sz.scale(til::math::ceiling, scale); + std::ignore = sz.scale(til::math::ceiling, scale); }; VERIFY_THROWS(fn(), gsl::narrowing_error); @@ -359,7 +359,7 @@ class SizeTests const til::size divisor{ 3, 2 }; auto fn = [&]() { - sz.divide_ceil(divisor); + std::ignore = sz.divide_ceil(divisor); }; VERIFY_THROWS(fn(), std::invalid_argument); From 47a17cf2d7b7762708d809fa80f87befce724d33 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 4 Apr 2023 17:19:20 +0200 Subject: [PATCH 008/226] Replace statics in headers with inline constants (#15100) C++ is a very well balanced and reasonable language, which is why `static` inside classes means "shared between all instances", whereas `static` outside of classes means "not shared between all .cpp files". 32 years after this problem was written onto parchment it was fixed with the introduction of inline variables in C++17, which tell the compiler to deduplicate variables the same way it deduplicates functions. --- src/cascadia/TerminalApp/TerminalPage.h | 6 +++--- src/cascadia/TerminalApp/TerminalTab.h | 6 +++--- src/cascadia/TerminalConnection/AzureClientID.h | 2 +- src/cascadia/TerminalCore/Terminal.hpp | 4 ++-- .../TerminalSettingsEditor/ColorSchemeViewModel.h | 12 ++++++------ .../TerminalSettingsModel/DynamicProfileUtils.h | 2 +- .../LegacyProfileGeneratorNamespaces.h | 6 +++--- src/cascadia/inc/WindowingBehavior.h | 14 +++++++------- src/inc/til/spsc.h | 6 +++--- src/inc/til/string.h | 2 +- src/renderer/inc/FontInfoBase.hpp | 4 ++-- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index efc68f5f6b2..b178389ec2b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -15,9 +15,6 @@ #define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); -static constexpr uint32_t DefaultRowsToScroll{ 3 }; -static constexpr std::wstring_view TabletInputServiceKey{ L"TabletInputService" }; - namespace TerminalAppLocalTests { class TabTests; @@ -31,6 +28,9 @@ namespace Microsoft::Terminal::Core namespace winrt::TerminalApp::implementation { + inline constexpr uint32_t DefaultRowsToScroll{ 3 }; + inline constexpr std::wstring_view TabletInputServiceKey{ L"TabletInputService" }; + enum StartupState : int { NotInitialized = 0, diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 66ae9e63c8b..7809b9c7612 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -7,9 +7,6 @@ #include "TabBase.h" #include "TerminalTab.g.h" -static constexpr double HeaderRenameBoxWidthDefault{ 165 }; -static constexpr double HeaderRenameBoxWidthTitleLength{ std::numeric_limits::infinity() }; - // fwdecl unittest classes namespace TerminalAppLocalTests { @@ -109,6 +106,9 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable); private: + static constexpr double HeaderRenameBoxWidthDefault{ 165 }; + static constexpr double HeaderRenameBoxWidthTitleLength{ std::numeric_limits::infinity() }; + std::shared_ptr _rootPane{ nullptr }; std::shared_ptr _activePane{ nullptr }; std::shared_ptr _zoomedPane{ nullptr }; diff --git a/src/cascadia/TerminalConnection/AzureClientID.h b/src/cascadia/TerminalConnection/AzureClientID.h index 253b9b9e34a..003e4d22b2c 100644 --- a/src/cascadia/TerminalConnection/AzureClientID.h +++ b/src/cascadia/TerminalConnection/AzureClientID.h @@ -3,4 +3,4 @@ #pragma once -static constexpr std::wstring_view AzureClientID = L"0"; +inline constexpr std::wstring_view AzureClientID = L"0"; diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 589b94b47bf..ebc3623dc03 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -17,8 +17,8 @@ #include -static constexpr std::wstring_view linkPattern{ LR"(\b(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$])" }; -static constexpr size_t TaskbarMinProgress{ 10 }; +inline constexpr std::wstring_view linkPattern{ LR"(\b(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$])" }; +inline constexpr size_t TaskbarMinProgress{ 10 }; // You have to forward decl the ICoreSettings here, instead of including the header. // If you include the header, there will be compilation errors with other diff --git a/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h b/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h index 6b31e06343b..6adb823ec85 100644 --- a/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h @@ -10,13 +10,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { - inline static constexpr uint8_t ColorTableDivider{ 8 }; - inline static constexpr uint8_t ColorTableSize{ 16 }; + inline constexpr uint8_t ColorTableDivider{ 8 }; + inline constexpr uint8_t ColorTableSize{ 16 }; - inline static constexpr std::wstring_view ForegroundColorTag{ L"Foreground" }; - inline static constexpr std::wstring_view BackgroundColorTag{ L"Background" }; - inline static constexpr std::wstring_view CursorColorTag{ L"CursorColor" }; - inline static constexpr std::wstring_view SelectionBackgroundColorTag{ L"SelectionBackground" }; + inline constexpr std::wstring_view ForegroundColorTag{ L"Foreground" }; + inline constexpr std::wstring_view BackgroundColorTag{ L"Background" }; + inline constexpr std::wstring_view CursorColorTag{ L"CursorColor" }; + inline constexpr std::wstring_view SelectionBackgroundColorTag{ L"SelectionBackground" }; struct ColorSchemeViewModel : ColorSchemeViewModelT, ViewModelHelper { diff --git a/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h b/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h index cf3e6e9e12d..1e97e0bae5b 100644 --- a/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h +++ b/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h @@ -21,6 +21,6 @@ Author(s): // will become disconnected from user settings. // {2bde4a90-d05f-401c-9492-e40884ead1d8} // uuidv5 properties: name format is UTF-16LE bytes -static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } }; +inline constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } }; winrt::com_ptr CreateDynamicProfile(const std::wstring_view& name); diff --git a/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h b/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h index 30d0f636aed..acb065fc0a9 100644 --- a/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h +++ b/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h @@ -16,6 +16,6 @@ Author(s): #pragma once -static constexpr std::wstring_view WslGeneratorNamespace{ L"Windows.Terminal.Wsl" }; -static constexpr std::wstring_view AzureGeneratorNamespace{ L"Windows.Terminal.Azure" }; -static constexpr std::wstring_view PowershellCoreGeneratorNamespace{ L"Windows.Terminal.PowershellCore" }; +inline constexpr std::wstring_view WslGeneratorNamespace{ L"Windows.Terminal.Wsl" }; +inline constexpr std::wstring_view AzureGeneratorNamespace{ L"Windows.Terminal.Azure" }; +inline constexpr std::wstring_view PowershellCoreGeneratorNamespace{ L"Windows.Terminal.PowershellCore" }; diff --git a/src/cascadia/inc/WindowingBehavior.h b/src/cascadia/inc/WindowingBehavior.h index 43515e7b468..348b2b51ab6 100644 --- a/src/cascadia/inc/WindowingBehavior.h +++ b/src/cascadia/inc/WindowingBehavior.h @@ -4,11 +4,11 @@ Licensed under the MIT license. --*/ #pragma once -constexpr int32_t WindowingBehaviorUseCurrent{ 0 }; -constexpr int32_t WindowingBehaviorUseNew{ -1 }; -constexpr int32_t WindowingBehaviorUseExisting{ -2 }; -constexpr int32_t WindowingBehaviorUseAnyExisting{ -3 }; -constexpr int32_t WindowingBehaviorUseName{ -4 }; -constexpr int32_t WindowingBehaviorUseNone{ -5 }; +inline constexpr int32_t WindowingBehaviorUseCurrent{ 0 }; +inline constexpr int32_t WindowingBehaviorUseNew{ -1 }; +inline constexpr int32_t WindowingBehaviorUseExisting{ -2 }; +inline constexpr int32_t WindowingBehaviorUseAnyExisting{ -3 }; +inline constexpr int32_t WindowingBehaviorUseName{ -4 }; +inline constexpr int32_t WindowingBehaviorUseNone{ -5 }; -static constexpr std::wstring_view QuakeWindowName{ L"_quake" }; +inline constexpr std::wstring_view QuakeWindowName{ L"_quake" }; diff --git a/src/inc/til/spsc.h b/src/inc/til/spsc.h index 366930693a7..9732cd5796d 100644 --- a/src/inc/til/spsc.h +++ b/src/inc/til/spsc.h @@ -24,9 +24,9 @@ namespace til::spsc namespace details { - static constexpr size_type position_mask = std::numeric_limits::max() >> 2u; // 0b00111.... - static constexpr size_type revolution_flag = 1u << (std::numeric_limits::digits - 2u); // 0b01000.... - static constexpr size_type drop_flag = 1u << (std::numeric_limits::digits - 1u); // 0b10000.... + inline constexpr size_type position_mask = std::numeric_limits::max() >> 2u; // 0b00111.... + inline constexpr size_type revolution_flag = 1u << (std::numeric_limits::digits - 2u); // 0b01000.... + inline constexpr size_type drop_flag = 1u << (std::numeric_limits::digits - 1u); // 0b10000.... struct block_initially_policy { diff --git a/src/inc/til/string.h b/src/inc/til/string.h index c6cfb28ce72..00b8c1e4eb3 100644 --- a/src/inc/til/string.h +++ b/src/inc/til/string.h @@ -36,7 +36,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" inline constexpr uint8_t F_ = 0b10; // stripped in clean_filename inline constexpr uint8_t _P = 0b01; // stripped in clean_path inline constexpr uint8_t FP = 0b11; // stripped in clean_filename and clean_path - static constexpr std::array pathFilter{ { + inline constexpr std::array pathFilter{ { // clang-format off __ /* NUL */, __ /* SOH */, __ /* STX */, __ /* ETX */, __ /* EOT */, __ /* ENQ */, __ /* ACK */, __ /* BEL */, __ /* BS */, __ /* HT */, __ /* LF */, __ /* VT */, __ /* FF */, __ /* CR */, __ /* SO */, __ /* SI */, __ /* DLE */, __ /* DC1 */, __ /* DC2 */, __ /* DC3 */, __ /* DC4 */, __ /* NAK */, __ /* SYN */, __ /* ETB */, __ /* CAN */, __ /* EM */, __ /* SUB */, __ /* ESC */, __ /* FS */, __ /* GS */, __ /* RS */, __ /* US */, diff --git a/src/renderer/inc/FontInfoBase.hpp b/src/renderer/inc/FontInfoBase.hpp index 2ef5478f84d..70ba4bd1202 100644 --- a/src/renderer/inc/FontInfoBase.hpp +++ b/src/renderer/inc/FontInfoBase.hpp @@ -20,8 +20,8 @@ Author(s): #include "IFontDefaultList.hpp" -static constexpr wchar_t DEFAULT_TT_FONT_FACENAME[]{ L"__DefaultTTFont__" }; -static constexpr wchar_t DEFAULT_RASTER_FONT_FACENAME[]{ L"Terminal" }; +inline constexpr wchar_t DEFAULT_TT_FONT_FACENAME[]{ L"__DefaultTTFont__" }; +inline constexpr wchar_t DEFAULT_RASTER_FONT_FACENAME[]{ L"Terminal" }; class FontInfoBase { From da995a014fb011e8c0a1975e961ff232a93601ba Mon Sep 17 00:00:00 2001 From: James Pack Date: Tue, 4 Apr 2023 12:24:33 -0400 Subject: [PATCH 009/226] Enable holding ctrl to open the Terminal elevated from File Explorer ## Summary of the Pull Request This pull request adds support for holding the control key and clicking the Open Terminal Here context menu item to elevate the request. ## References and Relevant Issues #14810 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed ## PR Checklist - [x] Closes #14810 - [ ] Tests added/passed - [ ] Documentation updated - If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx - [ ] Schema updated (if necessary) --------- Co-authored-by: Mike Griese --- .../ShellExtension/OpenTerminalHere.cpp | 26 +++++++++++++++++-- .../ShellExtension/OpenTerminalHere.h | 1 + src/cascadia/WinRTUtils/inc/WtExeUtils.h | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index 4aee550e5c8..34360d9d7e1 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -27,6 +27,8 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, IBindCtx* /*pBindContext*/) try { + const auto runElevated = IsControlPressed(); + wil::com_ptr_nothrow psi; RETURN_IF_FAILED(GetBestLocationFromSelectionOrSite(psiItemArray, psi.put())); if (!psi) @@ -42,10 +44,18 @@ try STARTUPINFOEX siEx{ 0 }; siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; std::wstring cmdline; - RETURN_IF_FAILED(wil::str_printf_nothrow(cmdline, LR"-("%s" -d %s)-", GetWtExePath().c_str(), QuoteAndEscapeCommandlineArg(pszName.get()).c_str())); + if (runElevated) + { + RETURN_IF_FAILED(wil::str_printf_nothrow(cmdline, LR"-(-d %s)-", QuoteAndEscapeCommandlineArg(pszName.get()).c_str())); + } + else + { + RETURN_IF_FAILED(wil::str_printf_nothrow(cmdline, LR"-("%s" -d %s)-", GetWtExePath().c_str(), QuoteAndEscapeCommandlineArg(pszName.get()).c_str())); + } RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( - nullptr, // lpApplicationName + runElevated ? modulePath.replace_filename(ElevateShimExe).c_str() : nullptr, // if elevation requested pass the elevate-shim.exe as the application name cmdline.data(), nullptr, // lpProcessAttributes nullptr, // lpThreadAttributes @@ -193,3 +203,15 @@ HRESULT OpenTerminalHere::GetBestLocationFromSelectionOrSite(IShellItemArray* ps RETURN_IF_FAILED(psi.copy_to(location)); return S_OK; } + +// This method checks if any of the ctrl keys are pressed during activation of the shell extension +bool OpenTerminalHere::IsControlPressed() +{ + const auto ControlPressed = 1U; + + const auto control = GetKeyState(VK_CONTROL); + const auto leftControl = GetKeyState(VK_LCONTROL); + const auto rightControl = GetKeyState(VK_RCONTROL); + + return WI_IsFlagSet(control, ControlPressed) || WI_IsFlagSet(leftControl, ControlPressed) || WI_IsFlagSet(rightControl, ControlPressed); +} diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.h b/src/cascadia/ShellExtension/OpenTerminalHere.h index bfcf660faee..f5b294942c8 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.h +++ b/src/cascadia/ShellExtension/OpenTerminalHere.h @@ -58,6 +58,7 @@ struct private: HRESULT GetLocationFromSite(IShellItem** location) const noexcept; HRESULT GetBestLocationFromSelectionOrSite(IShellItemArray* psiArray, IShellItem** location) const noexcept; + bool IsControlPressed(); wil::com_ptr_nothrow site_; }; diff --git a/src/cascadia/WinRTUtils/inc/WtExeUtils.h b/src/cascadia/WinRTUtils/inc/WtExeUtils.h index 0b0d40e23af..306af685e1f 100644 --- a/src/cascadia/WinRTUtils/inc/WtExeUtils.h +++ b/src/cascadia/WinRTUtils/inc/WtExeUtils.h @@ -2,6 +2,7 @@ constexpr std::wstring_view WtExe{ L"wt.exe" }; constexpr std::wstring_view WtdExe{ L"wtd.exe" }; constexpr std::wstring_view WindowsTerminalExe{ L"WindowsTerminal.exe" }; constexpr std::wstring_view LocalAppDataAppsPath{ L"%LOCALAPPDATA%\\Microsoft\\WindowsApps\\" }; +constexpr std::wstring_view ElevateShimExe{ L"elevate-shim.exe" }; _TIL_INLINEPREFIX bool IsPackaged() { From 2a839d8c5ad5a5b75a66c1eca14930afa8a45cf6 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 4 Apr 2023 18:33:17 +0200 Subject: [PATCH 010/226] Fix accuracy bugs around float/double/int conversions (#15098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I noticed this bug while resizing my window on my 150% scale display. Every 3 "snaps" of the window size, it would fail to resize the text buffer. I found that this occurs, because we convert the swap chain size from a float into a double, which converts my 597.333313 height into 597.33331298828125, which then multiplied by 1.5 results in 895.999969482421875. If you just cast this to an integer, it'll result in a height of 895px instead of the expected 896px. This PR addresses the issue in two ways: * Replace casts to integers with `lrint` or `floor`, etc. * Remove many of the redundant double <> float conversions. ## PR Checklist * Resizing my window always resizes the text buffer ✅ --- src/cascadia/TerminalControl/ControlCore.cpp | 37 +++++++++---------- src/cascadia/TerminalControl/ControlCore.h | 18 ++++----- src/cascadia/TerminalControl/ControlCore.idl | 12 +++--- src/cascadia/TerminalControl/TermControl.cpp | 30 +++++++-------- .../ControlInteractivityTests.cpp | 5 ++- src/cascadia/WindowsTerminal/AppHost.cpp | 10 ++--- src/inc/til/math.h | 2 +- 7 files changed, 55 insertions(+), 59 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 80205d6045a..18bcf861438 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -256,9 +256,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation _AttachedHandlers(*this, nullptr); } - bool ControlCore::Initialize(const double actualWidth, - const double actualHeight, - const double compositionScale) + bool ControlCore::Initialize(const float actualWidth, + const float actualHeight, + const float compositionScale) { assert(_settings); @@ -298,8 +298,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // and react accordingly. _updateFont(true); - const til::size windowSize{ static_cast(windowWidth), - static_cast(windowHeight) }; + const til::size windowSize{ til::math::rounding, windowWidth, windowHeight }; // First set up the dx engine with the window size in pixels. // Then, using the font, get the number of characters that can fit. @@ -853,8 +852,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // concerned with initialization process. Value forwarded to event handler. void ControlCore::_updateFont(const bool initialUpdate) { - const auto newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * - _compositionScale); + const auto newDpi = static_cast(lrint(_compositionScale * USER_DEFAULT_SCREEN_DPI)); _terminal->SetFontInfo(_actualFont); @@ -975,8 +973,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - auto cx = gsl::narrow_cast(_panelWidth * _compositionScale); - auto cy = gsl::narrow_cast(_panelHeight * _compositionScale); + auto cx = gsl::narrow_cast(lrint(_panelWidth * _compositionScale)); + auto cy = gsl::narrow_cast(lrint(_panelHeight * _compositionScale)); // Don't actually resize so small that a single character wouldn't fit // in either dimension. The buffer really doesn't like being size 0. @@ -986,7 +984,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Convert our new dimensions to characters const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, { cx, cy }); const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); - const auto currentVP = _terminal->GetViewport(); _terminal->ClearSelection(); @@ -1005,13 +1002,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - void ControlCore::SizeChanged(const double width, - const double height) + void ControlCore::SizeChanged(const float width, + const float height) { SizeOrScaleChanged(width, height, _compositionScale); } - void ControlCore::ScaleChanged(const double scale) + void ControlCore::ScaleChanged(const float scale) { if (!_renderEngine) { @@ -1020,24 +1017,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation SizeOrScaleChanged(_panelWidth, _panelHeight, scale); } - void ControlCore::SizeOrScaleChanged(const double width, - const double height, - const double scale) + void ControlCore::SizeOrScaleChanged(const float width, + const float height, + const float scale) { + const auto scaleChanged = _compositionScale != scale; // _refreshSizeUnderLock redraws the entire terminal. // Don't call it if we don't have to. - if (_panelWidth == width && _panelHeight == height && _compositionScale == scale) + if (_panelWidth == width && _panelHeight == height && !scaleChanged) { return; } - const auto oldScale = _compositionScale; _panelWidth = width; _panelHeight = height; _compositionScale = scale; auto lock = _terminal->LockForWriting(); - if (oldScale != scale) + if (scaleChanged) { // _updateFont relies on the new _compositionScale set above _updateFont(); @@ -1267,7 +1264,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Windows::Foundation::Size ControlCore::FontSizeInDips() const { const auto fontSize = _actualFont.GetSize(); - const auto scale = 1.0f / static_cast(_compositionScale); + const auto scale = 1.0f / _compositionScale; return { fontSize.width * scale, fontSize.height * scale, diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 8f7e40e6242..411c18ff88f 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -58,9 +58,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation TerminalConnection::ITerminalConnection connection); ~ControlCore(); - bool Initialize(const double actualWidth, - const double actualHeight, - const double compositionScale); + bool Initialize(const float actualWidth, + const float actualHeight, + const float compositionScale); void EnablePainting(); void Detach(); @@ -78,9 +78,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation uint64_t SwapChainHandle() const; void AttachToNewControl(const Microsoft::Terminal::Control::IKeyBindings& keyBindings); - void SizeChanged(const double width, const double height); - void ScaleChanged(const double scale); - void SizeOrScaleChanged(const double width, const double height, const double scale); + void SizeChanged(const float width, const float height); + void ScaleChanged(const float scale); + void SizeOrScaleChanged(const float width, const float height, const float scale); void AdjustFontSize(float fontSizeDelta); void ResetFontSize(); @@ -286,9 +286,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // These members represent the size of the surface that we should be // rendering to. - double _panelWidth{ 0 }; - double _panelHeight{ 0 }; - double _compositionScale{ 0 }; + float _panelWidth{ 0 }; + float _panelHeight{ 0 }; + float _compositionScale{ 0 }; uint64_t _owningHwnd{ 0 }; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 7110d9d342e..e5b5834745b 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -64,9 +64,9 @@ namespace Microsoft.Terminal.Control IControlAppearance unfocusedAppearance, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); - Boolean Initialize(Double actualWidth, - Double actualHeight, - Double compositionScale); + Boolean Initialize(Single actualWidth, + Single actualHeight, + Single compositionScale); void UpdateSettings(IControlSettings settings, IControlAppearance appearance); void ApplyAppearance(Boolean focused); @@ -108,9 +108,9 @@ namespace Microsoft.Terminal.Control void ResetFontSize(); void AdjustFontSize(Single fontSizeDelta); - void SizeChanged(Double width, Double height); - void ScaleChanged(Double scale); - void SizeOrScaleChanged(Double width, Double height, Double scale); + void SizeChanged(Single width, Single height); + void ScaleChanged(Single scale); + void SizeOrScaleChanged(Single width, Single height, Single scale); void ToggleShaderEffects(); void ToggleReadOnlyMode(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 5937f4436e9..d70b5c2e937 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -927,8 +927,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation return false; } - const auto panelWidth = SwapChainPanel().ActualWidth(); - const auto panelHeight = SwapChainPanel().ActualHeight(); + const auto panelWidth = static_cast(SwapChainPanel().ActualWidth()); + const auto panelHeight = static_cast(SwapChainPanel().ActualHeight()); const auto panelScaleX = SwapChainPanel().CompositionScaleX(); const auto panelScaleY = SwapChainPanel().CompositionScaleY(); @@ -2245,7 +2245,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // instantiating a DxEngine/AtlasEngine. // GH#10211 - UNDER NO CIRCUMSTANCE should this fail. If it does, the // whole app will crash instantaneously on launch, which is no good. - double scale; + float scale; if (settings.UseAtlasEngine()) { auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>(); @@ -2267,7 +2267,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // ComCtl scrollbars, but it's certainly close enough. const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); - double width = cols * actualFontSize.width; + float width = cols * static_cast(actualFontSize.width); // Reserve additional space if scrollbar is intended to be visible if (scrollState != ScrollbarState::Hidden) @@ -2275,13 +2275,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation width += scrollbarSize; } - double height = rows * actualFontSize.height; + float height = rows * static_cast(actualFontSize.height); const auto thickness = ParseThicknessFromPadding(padding); // GH#2061 - make sure to account for the size the padding _will be_ scaled to - width += scale * (thickness.Left + thickness.Right); - height += scale * (thickness.Top + thickness.Bottom); + width += scale * static_cast(thickness.Left + thickness.Right); + height += scale * static_cast(thickness.Top + thickness.Bottom); - return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; + return { width, height }; } // Method Description: @@ -2311,20 +2311,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_initializedTerminal) { const auto fontSize = _core.FontSize(); - double width = fontSize.Width; - double height = fontSize.Height; + auto width = fontSize.Width; + auto height = fontSize.Height; // Reserve additional space if scrollbar is intended to be visible if (_core.Settings().ScrollState() != ScrollbarState::Hidden) { - width += ScrollBar().ActualWidth(); + width += static_cast(ScrollBar().ActualWidth()); } // Account for the size of any padding const auto padding = GetPadding(); - width += padding.Left + padding.Right; - height += padding.Top + padding.Bottom; + width += static_cast(padding.Left + padding.Right); + height += static_cast(padding.Top + padding.Bottom); - return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; + return { width, height }; } else { @@ -2363,7 +2363,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto gridSize = dimension - nonTerminalArea; - const auto cells = static_cast(gridSize / fontDimension); + const auto cells = floor(gridSize / fontDimension); return cells * fontDimension + nonTerminalArea; } diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index 94dfb227a52..a48aa968944 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -1033,8 +1033,9 @@ namespace ControlUnitTests cursorPosition1.to_core_point()); Log::Comment(L" --- Resize the terminal to be 10 columns wider ---"); - const auto newSizeInDips{ til::size{ 40, 20 } * fontSize }; - core->SizeChanged(newSizeInDips.width, newSizeInDips.height); + const auto newWidth = 40.0f * fontSize.width; + const auto newHeight = 20.0f * fontSize.height; + core->SizeChanged(newWidth, newHeight); Log::Comment(L" --- Click on a spot that's NOW INSIDE the buffer ---"); // (32 + 35 + 1) = 68 = 'D' diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index df55bfa7e0f..2c5ba7ab153 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -579,10 +579,8 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, til::rect proposedRect, Launc auto initialSize = _windowLogic.GetLaunchDimensions(dpix); - const auto islandWidth = Utils::ClampToShortMax( - static_cast(ceil(initialSize.Width)), 1); - const auto islandHeight = Utils::ClampToShortMax( - static_cast(ceil(initialSize.Height)), 1); + const auto islandWidth = Utils::ClampToShortMax(lrintf(initialSize.Width), 1); + const auto islandHeight = Utils::ClampToShortMax(lrintf(initialSize.Height), 1); // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. @@ -1260,8 +1258,8 @@ void AppHost::_handleMoveContent(const winrt::Windows::Foundation::IInspectable& { const auto initialSize = _windowLogic.GetLaunchDimensions(dpi); - const auto islandWidth = Utils::ClampToShortMax(static_cast(ceil(initialSize.Width)), 1); - const auto islandHeight = Utils::ClampToShortMax(static_cast(ceil(initialSize.Height)), 1); + const auto islandWidth = Utils::ClampToShortMax(lrintf(initialSize.Width), 1); + const auto islandHeight = Utils::ClampToShortMax(lrintf(initialSize.Height), 1); // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. diff --git a/src/inc/til/math.h b/src/inc/til/math.h index 8f9a51e439d..183f9691ab1 100644 --- a/src/inc/til/math.h +++ b/src/inc/til/math.h @@ -17,7 +17,7 @@ namespace til constexpr O narrow_float(T val) { const auto o = gsl::narrow_cast(val); - if (std::isnan(val) || static_cast(o) != val) + if (static_cast(o) != val) { throw gsl::narrowing_error{}; } From 9dfdf2afa30bea4d70b48a589c29f15177d8fa20 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 4 Apr 2023 19:50:10 +0200 Subject: [PATCH 011/226] Introduce til::linear_flat_set (#15089) `til::linear_flat_set` is a primitive hash map with linear probing. The implementation is slightly complicated due to the use of templates. I've strongly considered just writing multiple copies of this class, by hand since the code is indeed fairly trivial but ended up deciding against it, because this templated approach makes testing easier. This class is in the order of 10x faster than `std::unordered_map`. --- .github/actions/spelling/expect/expect.txt | 1 + src/inc/til/flat_set.h | 141 ++++++++++++++++++ src/til/ut_til/FlatSetTests.cpp | 68 +++++++++ src/til/ut_til/til.unit.tests.vcxproj | 1 + src/til/ut_til/til.unit.tests.vcxproj.filters | 1 + tools/ConsoleTypes.natvis | 10 ++ 6 files changed, 222 insertions(+) create mode 100644 src/inc/til/flat_set.h create mode 100644 src/til/ut_til/FlatSetTests.cpp diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index e22a3c676b5..80cb7a963c0 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1124,6 +1124,7 @@ Mip MMBB mmcc MMCPL +MMIX mmsystem MNC MNOPQ diff --git a/src/inc/til/flat_set.h b/src/inc/til/flat_set.h new file mode 100644 index 00000000000..1a243639470 --- /dev/null +++ b/src/inc/til/flat_set.h @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#pragma warning(push) +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26409) // Avoid calling new and delete explicitly, use std::make_unique instead (r.11). + +namespace til +{ + // A simple hash function for simple hash maps. + // As demonstrated in https://doi.org/10.14778/2850583.2850585, a simple "multiply and shift" hash performs + // very well with linear probing hash maps and I found this to be true as well in my own testing. This hash + // function doesn't do the "shift" part, because linear_flat_set already does it by an appropriate amount. + constexpr size_t flat_set_hash_integer(size_t v) noexcept + { + // These two multipliers are the same as used by the PCG family of random number generators. + // The 32-Bit version is described in https://doi.org/10.1090/S0025-5718-99-00996-5, Table 5. + // The 64-Bit version is the multiplier as used by Donald Knuth for MMIX and found by C. E. Haynes. +#ifdef _WIN64 + return v * UINT64_C(6364136223846793005); +#else + return v * UINT32_C(747796405); +#endif + } + + // A basic, hashmap with linear probing. A `LoadFactor` of 2 equals + // a max. load of roughly 50% and a `LoadFactor` of 4 roughly 25%. + // + // It performs best with: + // * small and cheap T + // * >= 50% successful lookups + // * <= 50% load factor (LoadFactor >= 2, which is the minimum anyways) + template + struct linear_flat_set + { + static_assert(LoadFactor >= 2); + + bool empty() const noexcept + { + return _load == 0; + } + + size_t size() const noexcept + { + return _load / LoadFactor; + } + + std::span container() const noexcept + { + return { _map.get(), _capacity }; + } + + template + std::pair insert(U&& key) + { + // Putting this into the lookup path is a little pessimistic, but it + // allows us to default-construct this hashmap with a size of 0. + if (_load >= _capacity) [[unlikely]] + { + _bumpSize(); + } + + // The most common, basic and performant hash function is to multiply the value + // by some prime number and divide by the number of slots. It's been shown + // many times in literature that such a scheme performs the best on average. + // As such, we perform the divide here to get the topmost bits down. + // See flat_set_hash_integer. + const auto hash = ::std::hash{}(key) >> _shift; + + for (auto i = hash;; ++i) + { + auto& slot = _map[i & _mask]; + if (!slot) + { + slot = std::forward(key); + _load += LoadFactor; + return { slot, true }; + } + if (slot == key) [[likely]] + { + return { slot, false }; + } + } + } + + private: + __declspec(noinline) void _bumpSize() + { + // A _shift of 0 would result in a newShift of 0xfffff... + // A _shift of 1 would result in a newCapacity of 0 + if (_shift < 2) + { + throw std::bad_array_new_length{}; + } + + const auto newShift = _shift - 1; + const auto newCapacity = size_t{ 1 } << (digits - newShift); + const auto newMask = newCapacity - 1; + auto newMap = std::make_unique(newCapacity); + + // This mirrors the insert() function, but without the lookup part. + for (auto& oldSlot : container()) + { + if (!oldSlot) + { + continue; + } + + const auto hash = ::std::hash{}(oldSlot) >> newShift; + + for (auto i = hash;; ++i) + { + auto& slot = newMap[i & newMask]; + if (!slot) + { + slot = std::move_if_noexcept(oldSlot); + break; + } + } + } + + _map = std::move(newMap); + _capacity = newCapacity; + _shift = newShift; + _mask = newMask; + } + + static constexpr auto digits = std::numeric_limits::digits; + + std::unique_ptr _map; + size_t _capacity = 0; + size_t _load = 0; + // This results in an initial capacity of 8 items, independent of the LoadFactor. + size_t _shift = digits - LoadFactor - 1; + size_t _mask = 0; + }; +} + +#pragma warning(pop) diff --git a/src/til/ut_til/FlatSetTests.cpp b/src/til/ut_til/FlatSetTests.cpp new file mode 100644 index 00000000000..43e4c5fb2be --- /dev/null +++ b/src/til/ut_til/FlatSetTests.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +struct Data +{ + static constexpr auto emptyMarker = std::numeric_limits::max(); + + constexpr operator bool() const noexcept + { + return value != emptyMarker; + } + + constexpr bool operator==(int key) const noexcept + { + return value == static_cast(key); + } + + constexpr Data& operator=(int key) noexcept + { + value = static_cast(key); + return *this; + } + + size_t value = emptyMarker; +}; + +template<> +struct ::std::hash +{ + constexpr size_t operator()(int key) const noexcept + { + return til::flat_set_hash_integer(static_cast(key)); + } + + constexpr size_t operator()(Data d) const noexcept + { + return til::flat_set_hash_integer(d.value); + } +}; + +class FlatSetTests +{ + TEST_CLASS(FlatSetTests); + + TEST_METHOD(Basic) + { + til::linear_flat_set set; + + // This simultaneously demonstrates how the class can't just do "heterogeneous lookups" + // like STL does, but also insert items with a different type. + const auto [entry1, inserted1] = set.insert(123); + VERIFY_IS_TRUE(inserted1); + + const auto [entry2, inserted2] = set.insert(123); + VERIFY_IS_FALSE(inserted2); + + VERIFY_ARE_EQUAL(&entry1, &entry2); + VERIFY_ARE_EQUAL(123u, entry2.value); + } +}; diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index 824bf40bd4d..04694e68a16 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -20,6 +20,7 @@ + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index 545bc412f55..c25b0113980 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -29,6 +29,7 @@ + diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis index 803bf5d3337..de0c91ffd23 100644 --- a/tools/ConsoleTypes.natvis +++ b/tools/ConsoleTypes.natvis @@ -112,4 +112,14 @@ _value + + + {{ size={_load / $T2} }} + + + _capacity + _map._Mypair._Myval2 + + + From 06526cac0c8a7e1a63fd187ce495e3b88522adff Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 4 Apr 2023 16:11:12 -0500 Subject: [PATCH 012/226] Remove our dependency on any CRT--AppX or forwarders (#15097) The upgrade to Microsoft.UI.Xaml 2.8 was the last piece we needed to break our dependency on the App CRT *and* any CRT whatsoever. --- build/scripts/Test-WindowsTerminalPackage.ps1 | 25 ++++++++++--------- dep/nuget/packages.config | 1 - .../WindowExe/WindowExe.vcxproj | 2 -- .../WindowExe/packages.config | 1 - .../CascadiaPackage/CascadiaPackage.wapproj | 4 +++ .../WindowsTerminal/WindowsTerminal.vcxproj | 1 - src/common.nugetversions.targets | 6 ----- 7 files changed, 17 insertions(+), 23 deletions(-) diff --git a/build/scripts/Test-WindowsTerminalPackage.ps1 b/build/scripts/Test-WindowsTerminalPackage.ps1 index a33329c26cd..b405d98c212 100644 --- a/build/scripts/Test-WindowsTerminalPackage.ps1 +++ b/build/scripts/Test-WindowsTerminalPackage.ps1 @@ -70,23 +70,24 @@ Try { $dependencies = $Manifest.Package.Dependencies.PackageDependency.Name $depsHasVclibsDesktop = ("Microsoft.VCLibs.140.00.UWPDesktop" -in $dependencies) -or ("Microsoft.VCLibs.140.00.Debug.UWPDesktop" -in $dependencies) - $depsHasVcLibsAppX = ("Microsoft.VCLibs.140.00" -in $dependencies) -or ("Microsoft.VCLibs.140.00.Debug" -in $dependencies) + $depsHasVclibsAppX = ("Microsoft.VCLibs.140.00" -in $dependencies) -or ("Microsoft.VCLibs.140.00.Debug" -in $dependencies) $filesHasVclibsDesktop = ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140.dll" -EA:Ignore)) -or ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140d.dll" -EA:Ignore)) $filesHasVclibsAppX = ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140_app.dll" -EA:Ignore)) -or ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140d_app.dll" -EA:Ignore)) - If ($depsHasVclibsDesktop -Eq $filesHasVclibsDesktop) { - $eitherBoth = if ($depsHasVclibsDesktop) { "both" } else { "neither" } - $neitherNor = if ($depsHasVclibsDesktop) { "and" } else { "nor" } - Throw "Package has $eitherBoth Dependency $neitherNor Integrated Desktop VCLibs" + If ($filesHasVclibsDesktop) { + Throw "Package contains the desktop VCLibs" } - If ($depsHasVclibsAppx -Eq $filesHasVclibsAppx) { - if ($depsHasVclibsAppx) { - # We've shipped like this forever, so downgrade to warning. - Write-Warning "Package has both Dependency and Integrated AppX VCLibs" - } else { - Throw "Package has neither Dependency nor Integrated AppX VCLibs" - } + If ($depsHasVclibsDesktop) { + Throw "Package has a dependency on the desktop VCLibs" + } + + If ($filesHasVclibsAppX) { + Throw "Package contains the AppX VCLibs" + } + + If ($depsHasVclibsAppX) { + Throw "Package has a dependency on the AppX VCLibs" } ### Check that we have an App.xbf (which is a proxy for our resources having been merged) diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index 09a9c4935d4..c39e95a3c85 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -5,7 +5,6 @@ - diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj index de4c7a34351..fa14a3b19a8 100644 --- a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj @@ -17,8 +17,6 @@ true - true - true true diff --git a/scratch/ScratchIslandApp/WindowExe/packages.config b/scratch/ScratchIslandApp/WindowExe/packages.config index 040e8f88beb..3543aaf75ba 100644 --- a/scratch/ScratchIslandApp/WindowExe/packages.config +++ b/scratch/ScratchIslandApp/WindowExe/packages.config @@ -3,5 +3,4 @@ - diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 25c1b9c5f71..58be390d1c6 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -153,6 +153,8 @@ + + @@ -161,6 +163,8 @@ + + diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index 2379a6430a2..62875db2b6e 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -18,7 +18,6 @@ true - true true true diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index e047b9931d2..7cffff5ec83 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -44,9 +44,6 @@ - - - @@ -84,9 +81,6 @@ - - - From aea0477bda6a02889913d75031b8fe183caaf3d6 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Wed, 5 Apr 2023 15:58:52 +0100 Subject: [PATCH 013/226] Filter out control characters that don't do anything (#15075) On a real VT terminal, most of the control characters that don't do anything are supposed to be filtered out, and not written to the buffer. Up to to now, though, we've only been filtering out `NUL`. This PR extends our control processing to filter the remaining characters that aren't supposed to be displayed. We introduced filtering for the `NUL` control in PR #3015. The are two special cases worth mentioning. 1. The `SUB` control's main purpose is to the cancel a control sequence that is in progress, but it also needs to output an error character (a reverse question mark) to the display. 2. The `DEL` control is typically filtered out, but when a 96-character set is designated, it can sometimes be mapped to a printable glyph that needs to be displayed. ## Validation Steps Performed I've manually tested that all the controls that are meant to be filtered out are no longer being displayed. I've also extended the existing `NUL` unit test to cover the full set of controls characters that are supposed to be filtered. Closes #10786 --- .github/actions/spelling/expect/expect.txt | 1 + src/host/ut_host/ScreenBufferTests.cpp | 37 ++++++++++++------- .../parser/OutputStateMachineEngine.cpp | 25 ++++++++++--- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 80cb7a963c0..1d6272fc2d6 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -38,6 +38,7 @@ ansicpg ANSISYS ANSISYSRC ANSISYSSC +answerback antialiasing ANull anycpu diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index c7856418cca..48d4e277190 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -118,7 +118,7 @@ class ScreenBufferTests TEST_METHOD(EraseAllTests); - TEST_METHOD(OutputNULTest); + TEST_METHOD(InactiveControlCharactersTest); TEST_METHOD(VtResize); TEST_METHOD(VtResizeComprehensive); @@ -1014,8 +1014,19 @@ void ScreenBufferTests::EraseAllTests() viewport.BottomInclusive())); } -void ScreenBufferTests::OutputNULTest() +void ScreenBufferTests::InactiveControlCharactersTest() { + // These are the control characters that don't write anything to the + // output buffer, and are expected not to move the cursor position. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:ordinal", L"{0, 1, 2, 3, 4, 5, 6, 7, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31}") + END_TEST_METHOD_PROPERTIES() + + unsigned ordinal; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"ordinal", ordinal)); + const auto ch = static_cast(ordinal); + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); @@ -1024,21 +1035,20 @@ void ScreenBufferTests::OutputNULTest() VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Writing a single NUL")); - stateMachine.ProcessString({ L"\0", 1 }); + Log::Comment(L"Writing a single control character"); + const auto singleChar = std::wstring(1, ch); + stateMachine.ProcessString(singleChar); VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Writing many NULs")); - stateMachine.ProcessString({ L"\0\0\0\0\0\0\0\0", 8 }); + Log::Comment(L"Writing many control characters"); + const auto manyChars = std::wstring(8, ch); + stateMachine.ProcessString(manyChars); VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Testing a single NUL followed by real text")); - stateMachine.ProcessString({ L"\0foo", 4 }); + Log::Comment(L"Testing a single control character followed by real text"); + stateMachine.ProcessString(singleChar + L"foo"); VERIFY_ARE_EQUAL(3, cursor.GetPosition().x); VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); @@ -1046,9 +1056,8 @@ void ScreenBufferTests::OutputNULTest() VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Writing NULs in between other strings")); - stateMachine.ProcessString({ L"\0foo\0bar\0", 9 }); + Log::Comment(L"Writing controls in between other strings"); + stateMachine.ProcessString(singleChar + L"foo" + singleChar + L"bar" + singleChar); VERIFY_ARE_EQUAL(6, cursor.GetPosition().x); VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); } diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 3f191e34198..56eb7b0f091 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -44,10 +44,10 @@ bool OutputStateMachineEngine::ActionExecute(const wchar_t wch) { switch (wch) { - case AsciiChars::NUL: - // microsoft/terminal#1825 - VT applications expect to be able to write NUL - // and have _nothing_ happen. Filter the NULs here, so they don't fill the - // buffer with empty spaces. + case AsciiChars::ENQ: + // GH#11946: At some point we may want to add support for the VT + // answerback feature, which requires responding to an ENQ control + // with a user-defined reply, but until then we just ignore it. break; case AsciiChars::BEL: _dispatch->WarningBell(); @@ -79,9 +79,24 @@ bool OutputStateMachineEngine::ActionExecute(const wchar_t wch) case AsciiChars::SO: _dispatch->LockingShift(1); break; - default: + case AsciiChars::SUB: + // The SUB control is used to cancel a control sequence in the same + // way as CAN, but unlike CAN it also displays an error character, + // typically a reverse question mark. + _dispatch->Print(L'\u2E2E'); + break; + case AsciiChars::DEL: + // The DEL control can sometimes be translated into a printable glyph + // if a 96-character set is designated, so we need to pass it through + // to the Print method. If not translated, it will be filtered out + // there. _dispatch->Print(wch); break; + default: + // GH#1825, GH#10786: VT applications expect to be able to write other + // control characters and have _nothing_ happen. We filter out these + // characters here, so they don't fill the buffer. + break; } _ClearLastChar(); From ecb5e37a7da8cd039b37aa8b6a742d82752c44f9 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 5 Apr 2023 16:59:20 +0200 Subject: [PATCH 014/226] Use new row primitives for ResizeTraditional (#15105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will allow us to share the same fundamental text insertion logic for both `ResizeTraditional` and `Reflow`, because both can be implemented with `ROW::CopyRangeFrom`. It also replaces the `BufferAllocator` struct with a `_allocateBuffer` function which will help us allocate scratch buffer rows in the future. Closes #14696 ## PR Checklist * Disable reflow resize in conhost * Print "zhwik8.txt" - a enwik8.txt equivalent of Chinese Wikipedia * Run `color 80` in cmd * Resize windows from 120 to 119 columns * Wide glyphs disappear and are replaced with whitespace ✅ * Resizing the window to >120 columns adds gray whitespace ✅ --- src/buffer/out/Row.cpp | 85 ----------------- src/buffer/out/Row.hpp | 5 +- src/buffer/out/textBuffer.cpp | 169 ++++++++++++++-------------------- src/buffer/out/textBuffer.hpp | 2 + 4 files changed, 73 insertions(+), 188 deletions(-) diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 38a82857cf2..9d883b09502 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -88,19 +88,6 @@ ROW::ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, c } } -void swap(ROW& lhs, ROW& rhs) noexcept -{ - std::swap(lhs._charsBuffer, rhs._charsBuffer); - std::swap(lhs._charsHeap, rhs._charsHeap); - std::swap(lhs._chars, rhs._chars); - std::swap(lhs._charOffsets, rhs._charOffsets); - std::swap(lhs._attr, rhs._attr); - std::swap(lhs._columnCount, rhs._columnCount); - std::swap(lhs._lineRendition, rhs._lineRendition); - std::swap(lhs._wrapForced, rhs._wrapForced); - std::swap(lhs._doubleBytePadded, rhs._doubleBytePadded); -} - void ROW::SetWrapForced(const bool wrap) noexcept { _wrapForced = wrap; @@ -154,78 +141,6 @@ void ROW::_init() noexcept std::iota(_charOffsets.begin(), _charOffsets.end(), uint16_t{ 0 }); } -// Routine Description: -// - resizes ROW to new width -// Arguments: -// - charsBuffer - a new backing buffer to use for _charsBuffer -// - charOffsetsBuffer - a new backing buffer to use for _charOffsets -// - rowWidth - the new width, in cells -// - fillAttribute - the attribute to use for any newly added, trailing cells -void ROW::Resize(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute) -{ - // A default-constructed ROW has no cols/chars to copy. - // It can be detected by the lack of a _charsBuffer (among others). - // - // Otherwise, this block figures out how much we can copy into the new `rowWidth`. - uint16_t colsToCopy = 0; - uint16_t charsToCopy = 0; - if (_charsBuffer) - { - colsToCopy = std::min(rowWidth, _columnCount); - // Safety: colsToCopy is [0, _columnCount]. - charsToCopy = _uncheckedCharOffset(colsToCopy); - // Safety: colsToCopy is [0, _columnCount] due to colsToCopy != 0. - for (; colsToCopy != 0 && _uncheckedIsTrailer(colsToCopy); --colsToCopy) - { - } - } - - // If we grow the row width, we have to append a bunch of whitespace. - // `trailingWhitespace` stores that amount. - // Safety: The preceding block left colsToCopy in the range [0, rowWidth]. - const uint16_t trailingWhitespace = rowWidth - colsToCopy; - - // Allocate memory for the new `_chars` array. - // Use the provided charsBuffer if possible, otherwise allocate a `_charsHeap`. - std::unique_ptr charsHeap; - std::span chars{ charsBuffer, rowWidth }; - const std::span charOffsets{ charOffsetsBuffer, ::base::strict_cast(rowWidth) + 1u }; - if (const uint16_t charsCapacity = charsToCopy + trailingWhitespace; charsCapacity > rowWidth) - { - charsHeap = std::make_unique_for_overwrite(charsCapacity); - chars = { charsHeap.get(), charsCapacity }; - } - - // Copy chars and charOffsets over. - { - const auto it = std::copy_n(_chars.begin(), charsToCopy, chars.begin()); - std::fill_n(it, trailingWhitespace, L' '); - } - { - const auto it = std::copy_n(_charOffsets.begin(), colsToCopy, charOffsets.begin()); - // The _charOffsets array is 1 wider than newWidth indicates. - // This is because the extra column contains the past-the-end index into _chars. - iota_n(it, trailingWhitespace + 1u, charsToCopy); - } - - _charsBuffer = charsBuffer; - _charsHeap = std::move(charsHeap); - _chars = chars; - _charOffsets = charOffsets; - _columnCount = rowWidth; - - // .resize_trailing_extent() doesn't work if the vector is empty, - // since there's no trailing item that could be extended. - if (_attr.empty()) - { - _attr = { rowWidth, fillAttribute }; - } - else - { - _attr.resize_trailing_extent(rowWidth); - } -} - void ROW::TransferAttributes(const til::small_rle& attr, til::CoordType newWidth) { _attr = attr; diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index 1e14c382fc5..c86312984e9 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -66,11 +66,9 @@ class ROW final ROW(const ROW& other) = delete; ROW& operator=(const ROW& other) = delete; - explicit ROW(ROW&& other) = default; + ROW(ROW&& other) = default; ROW& operator=(ROW&& other) = default; - friend void swap(ROW& lhs, ROW& rhs) noexcept; - void SetWrapForced(const bool wrap) noexcept; bool WasWrapForced() const noexcept; void SetDoubleBytePadded(const bool doubleBytePadded) noexcept; @@ -79,7 +77,6 @@ class ROW final LineRendition GetLineRendition() const noexcept; void Reset(const TextAttribute& attr); - void Resize(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute); void TransferAttributes(const til::small_rle& attr, til::CoordType newWidth); til::CoordType NavigateToPrevious(til::CoordType column) const noexcept; diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 8b8f9d1f79a..9e969c6ed24 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -13,75 +13,6 @@ #include "../types/inc/convert.hpp" #include "../../types/inc/GlyphWidth.hpp" -namespace -{ - struct BufferAllocator - { - BufferAllocator(til::size sz) - { - const auto w = gsl::narrow(sz.width); - const auto h = gsl::narrow(sz.height); - - const auto charsBytes = w * sizeof(wchar_t); - // The ROW::_indices array stores 1 more item than the buffer is wide. - // That extra column stores the past-the-end _chars pointer. - const auto indicesBytes = w * sizeof(uint16_t) + sizeof(uint16_t); - const auto rowStride = charsBytes + indicesBytes; - // 65535*65535 cells would result in a charsAreaSize of 8GiB. - // --> Use uint64_t so that we can safely do our calculations even on x86. - const auto allocSize = gsl::narrow(::base::strict_cast(rowStride) * ::base::strict_cast(h)); - - _buffer = wil::unique_virtualalloc_ptr{ static_cast(VirtualAlloc(nullptr, allocSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) }; - THROW_IF_NULL_ALLOC(_buffer); - - _data = std::span{ _buffer.get(), allocSize }.begin(); - _rowStride = rowStride; - _indicesOffset = charsBytes; - _width = w; - _height = h; - } - - BufferAllocator& operator++() noexcept - { - _data += _rowStride; - return *this; - } - - wchar_t* chars() const noexcept - { - return til::bit_cast(&*_data); - } - - uint16_t* indices() const noexcept - { - return til::bit_cast(&*(_data + _indicesOffset)); - } - - uint16_t width() const noexcept - { - return _width; - } - - uint16_t height() const noexcept - { - return _height; - } - - wil::unique_virtualalloc_ptr&& take() noexcept - { - return std::move(_buffer); - } - - private: - wil::unique_virtualalloc_ptr _buffer; - std::span::iterator _data; - size_t _rowStride; - size_t _indicesOffset; - uint16_t _width; - uint16_t _height; - }; -} - using namespace Microsoft::Console; using namespace Microsoft::Console::Types; @@ -111,16 +42,7 @@ TextBuffer::TextBuffer(til::size screenBufferSize, // Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text. screenBufferSize.width = std::max(screenBufferSize.width, 1); screenBufferSize.height = std::max(screenBufferSize.height, 1); - - BufferAllocator allocator{ screenBufferSize }; - - _storage.reserve(allocator.height()); - for (til::CoordType i = 0; i < screenBufferSize.height; ++i, ++allocator) - { - _storage.emplace_back(allocator.chars(), allocator.indices(), allocator.width(), _currentAttributes); - } - - _charBuffer = allocator.take(); + _charBuffer = _allocateBuffer(screenBufferSize, _currentAttributes, _storage); _UpdateSize(); } @@ -775,6 +697,37 @@ const Viewport TextBuffer::GetSize() const noexcept return _size; } +wil::unique_virtualalloc_ptr TextBuffer::_allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector& rows) +{ + const auto w = gsl::narrow(sz.width); + const auto h = gsl::narrow(sz.height); + + const auto charsBytes = w * sizeof(wchar_t); + // The ROW::_indices array stores 1 more item than the buffer is wide. + // That extra column stores the past-the-end _chars pointer. + const auto indicesBytes = w * sizeof(uint16_t) + sizeof(uint16_t); + const auto rowStride = charsBytes + indicesBytes; + // 65535*65535 cells would result in a charsAreaSize of 8GiB. + // --> Use uint64_t so that we can safely do our calculations even on x86. + const auto allocSize = gsl::narrow(::base::strict_cast(rowStride) * ::base::strict_cast(h)); + + auto buffer = wil::unique_virtualalloc_ptr{ static_cast(VirtualAlloc(nullptr, allocSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) }; + THROW_IF_NULL_ALLOC(buffer); + + auto data = std::span{ buffer.get(), allocSize }.begin(); + + rows.resize(h); + for (auto& row : rows) + { + const auto chars = til::bit_cast(&*data); + const auto indices = til::bit_cast(&*(data + charsBytes)); + row = { chars, indices, w, attributes }; + data += rowStride; + } + + return buffer; +} + void TextBuffer::_UpdateSize() { _size = Viewport::FromDimensions({ _storage.at(0).size(), gsl::narrow(_storage.size()) }); @@ -1001,37 +954,55 @@ void TextBuffer::Reset() try { - BufferAllocator allocator{ newSize }; - - const auto currentSize = GetSize().Dimensions(); - const auto attributes = GetCurrentAttributes(); - til::CoordType TopRow = 0; // new top row of the screen buffer if (newSize.height <= GetCursor().GetPosition().y) { TopRow = GetCursor().GetPosition().y - newSize.height + 1; } - const auto TopRowIndex = (GetFirstRowIndex() + TopRow) % currentSize.height; + const auto TopRowIndex = gsl::narrow_cast(_firstRow + TopRow) % _storage.size(); - // rotate rows until the top row is at index 0 - std::rotate(_storage.begin(), _storage.begin() + TopRowIndex, _storage.end()); - _SetFirstRowIndex(0); - - // realloc in the Y direction - // remove rows if we're shrinking - _storage.resize(allocator.height()); + std::vector newStorage; + auto newBuffer = _allocateBuffer(newSize, _currentAttributes, newStorage); - // realloc in the X direction - for (auto& it : _storage) + // This basically imitates a std::rotate_copy(first, mid, last), but uses ROW::CopyRangeFrom() to do the copying. { - it.Resize(allocator.chars(), allocator.indices(), allocator.width(), attributes); - ++allocator; + const auto first = _storage.begin(); + const auto last = _storage.end(); + const auto mid = first + TopRowIndex; + auto dest = newStorage.begin(); + + std::span sourceRanges[]{ + { mid, last }, + { first, mid }, + }; + + // Ensure we don't copy more from `_storage` than fit into `newStorage`. + if (sourceRanges[0].size() > newStorage.size()) + { + sourceRanges[0] = sourceRanges[0].subspan(0, newStorage.size()); + } + if (const auto remaining = newStorage.size() - sourceRanges[0].size(); sourceRanges[1].size() > remaining) + { + sourceRanges[1] = sourceRanges[1].subspan(0, remaining); + } + + for (const auto& sourceRange : sourceRanges) + { + for (const auto& oldRow : sourceRange) + { + til::CoordType begin = 0; + dest->CopyRangeFrom(0, til::CoordTypeMax, oldRow, begin, til::CoordTypeMax); + dest->TransferAttributes(oldRow.Attributes(), newSize.width); + ++dest; + } + } } - // Update the cached size value - _UpdateSize(); + _charBuffer = std::move(newBuffer); + _storage = std::move(newStorage); - _charBuffer = allocator.take(); + _SetFirstRowIndex(0); + _UpdateSize(); } CATCH_RETURN(); diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index fe27d4ef007..555531a25aa 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -219,6 +219,8 @@ class TextBuffer final interval_tree::IntervalTree GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const; private: + static wil::unique_virtualalloc_ptr _allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector& rows); + void _UpdateSize(); void _SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept; til::point _GetPreviousFromCursor() const noexcept; From 62448969b30325a75f35201427a052de0e4dbb7d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 5 Apr 2023 17:03:20 +0200 Subject: [PATCH 015/226] Upgrade clang-format to 15.0.7 (#15110) Upgrading clang-format lead to a few changes in the formatting of code inside macros. Apart from the upgrade, I've also spent some time removing all options from .clang-format that are redundant with `BasedOnStyle: Microsoft`. --- .clang-format | 60 +-------- .github/actions/spelling/expect/expect.txt | 3 +- src/cascadia/TerminalControl/ControlCore.h | 20 +-- .../TerminalControl/ControlSettings.h | 12 +- .../TerminalSettingsEditor/ViewModelHelpers.h | 42 ++++--- .../ViewModelHelpers.idl.h | 5 +- .../TerminalSettingsModel/ActionArgs.h | 18 ++- .../TerminalSettingsModel/IInheritable.idl.h | 5 +- src/cascadia/inc/cppwinrt_utils.h | 116 +++++++++++++----- src/inc/til/bytes.h | 2 +- src/inc/til/pmr.h | 4 +- src/renderer/atlas/AtlasEngine.h | 37 ++++-- tools/packages.config | 2 +- 13 files changed, 188 insertions(+), 138 deletions(-) diff --git a/.clang-format b/.clang-format index a6db19a0688..1add70421ef 100644 --- a/.clang-format +++ b/.clang-format @@ -1,57 +1,25 @@ - +--- +Language: Cpp +BasedOnStyle: Microsoft AccessModifierOffset: -4 -AlignAfterOpenBracket: Align -AllowAllArgumentsOnNextLine: true -AlignConsecutiveMacros: false -AlignConsecutiveAssignments: false -AlignConsecutiveDeclarations: false -AllowAllConstructorInitializersOnNextLine: true AlignEscapedNewlines: Left -AlignOperands: true AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: Never AllowShortFunctionsOnASingleLine: All -AllowShortCaseLabelsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: Never -#AllowShortLambdasOnASingleLine: Inline -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BinPackArguments: false BinPackParameters: false BraceWrapping: AfterCaseLabel: true - AfterClass: true - AfterControlStatement: true - AfterEnum: true - AfterFunction: true - AfterNamespace: true - AfterObjCDeclaration: true - AfterStruct: true AfterUnion: true AfterExternBlock: false - BeforeCatch: true - BeforeElse: true - IndentBraces: false - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true -BreakBeforeBinaryOperators: None -BreakBeforeBraces: Custom BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon BreakInheritanceList: AfterColon ColumnLimit: 0 CommentPragmas: "suppress" -CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true -ConstructorInitializerIndentWidth: 4 -ContinuationIndentWidth: 4 Cpp11BracedListStyle: false -DeriveLineEnding: true -DerivePointerAlignment: false FixNamespaceComments: false IncludeBlocks: Regroup IncludeCategories: @@ -63,35 +31,13 @@ IncludeCategories: Priority: 2 - Regex: '.*' Priority: 3 -IndentCaseLabels: false -IndentPPDirectives: None -IndentWidth: 4 -IndentWrappedFunctionNames: false KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: "BEGIN_TEST_METHOD_PROPERTIES|BEGIN_MODULE|BEGIN_TEST_CLASS|BEGIN_TEST_METHOD" MacroBlockEnd: "END_TEST_METHOD_PROPERTIES|END_MODULE|END_TEST_CLASS|END_TEST_METHOD" -MaxEmptyLinesToKeep: 1 NamespaceIndentation: All PointerAlignment: Left ReflowComments: false SortIncludes: false -SortUsingDeclarations: true -SpaceAfterCStyleCast: false -SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: false -SpaceBeforeAssignmentOperators: true -SpaceBeforeCpp11BracedList: false -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatements -SpaceBeforeRangeBasedForLoopColon: true -SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 1 SpacesInAngles: false -SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false -SpacesInParentheses: false -SpacesInSquareBrackets: false -Standard: Latest -TabWidth: 4 -UseTab: Never diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 1d6272fc2d6..92b2cadbe02 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -177,7 +177,6 @@ CConsole CConversion CCRT cdd -CDeclaration CEdit CELLSIZE cfae @@ -333,7 +332,7 @@ Cspace csrmsg CSRSS csrutil -cstyle +CSTYLE CSwitch CTerminal CText diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 411c18ff88f..1d11495d8d5 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -32,13 +32,19 @@ namespace ControlUnitTests class ControlInteractivityTests; }; -#define RUNTIME_SETTING(type, name, setting) \ -private: \ - std::optional _runtime##name{ std::nullopt }; \ - void name(const type newValue) { _runtime##name = newValue; } \ - \ -public: \ - type name() const { return til::coalesce_value(_runtime##name, setting); } +#define RUNTIME_SETTING(type, name, setting) \ +private: \ + std::optional _runtime##name{ std::nullopt }; \ + void name(const type newValue) \ + { \ + _runtime##name = newValue; \ + } \ + \ +public: \ + type name() const \ + { \ + return til::coalesce_value(_runtime##name, setting); \ + } namespace winrt::Microsoft::Terminal::Control::implementation { diff --git a/src/cascadia/TerminalControl/ControlSettings.h b/src/cascadia/TerminalControl/ControlSettings.h index 6dc9a1f639e..0423b36f7b7 100644 --- a/src/cascadia/TerminalControl/ControlSettings.h +++ b/src/cascadia/TerminalControl/ControlSettings.h @@ -65,9 +65,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation // appearance is being used should be more careful. Fortunately, this // situation is generally only used when a control is first created, or // when calling UpdateSettings. -#define APPEARANCE_GEN(type, name, ...) \ - type name() const noexcept { return _focusedAppearance->name(); } \ - void name(const type& value) noexcept { _focusedAppearance->name(value); } +#define APPEARANCE_GEN(type, name, ...) \ + type name() const noexcept \ + { \ + return _focusedAppearance->name(); \ + } \ + void name(const type& value) noexcept \ + { \ + _focusedAppearance->name(value); \ + } CORE_APPEARANCE_SETTINGS(APPEARANCE_GEN) CONTROL_APPEARANCE_SETTINGS(APPEARANCE_GEN) diff --git a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h index a131a530971..e306233ab63 100644 --- a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h +++ b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h @@ -37,19 +37,25 @@ struct ViewModelHelper winrt::event<::winrt::Windows::UI::Xaml::Data::PropertyChangedEventHandler> _propertyChangedHandlers; }; -#define _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \ -public: \ - auto name() const noexcept { return target.name(); }; \ - template \ - void name(const T& value) \ - { \ - if (name() != value) \ - { \ - target.name(value); \ - _NotifyChanges(L"Has" #name, L## #name); \ - } \ - } \ - bool Has##name() { return target.Has##name(); } +#define _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \ +public: \ + auto name() const noexcept \ + { \ + return target.name(); \ + }; \ + template \ + void name(const T& value) \ + { \ + if (name() != value) \ + { \ + target.name(value); \ + _NotifyChanges(L"Has" #name, L## #name); \ + } \ + } \ + bool Has##name() \ + { \ + return target.Has##name(); \ + } // Defines a setting that reflects another object's same-named // setting. @@ -64,7 +70,10 @@ public: \ _NotifyChanges(L"Has" #name, L## #name); \ } \ } \ - auto name##OverrideSource() { return target.name##OverrideSource(); } + auto name##OverrideSource() \ + { \ + return target.name##OverrideSource(); \ + } // Defines a setting that reflects another object's same-named // setting, but which cannot be erased. @@ -76,7 +85,10 @@ public: \ // except it leverages _NotifyChanges. #define VIEW_MODEL_OBSERVABLE_PROPERTY(type, name, ...) \ public: \ - type name() const noexcept { return _##name; }; \ + type name() const noexcept \ + { \ + return _##name; \ + }; \ void name(const type& value) \ { \ if (_##name != value) \ diff --git a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h index 9579f2e7c06..f0ea1115076 100644 --- a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h +++ b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h @@ -9,7 +9,10 @@ get; \ set; \ }; \ - Boolean Has##Name { get; } + Boolean Has##Name \ + { \ + get; \ + } #define OBSERVABLE_PROJECTED_SETTING(Type, Name) \ _BASE_OBSERVABLE_PROJECTED_SETTING(Type, Name); \ diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 7f2fde239e3..cc598dbb6ad 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -56,12 +56,18 @@ #include "ActionArgsMagic.h" -#define ACTION_ARG(type, name, ...) \ -public: \ - type name() const noexcept { return _##name.has_value() ? _##name.value() : type{ __VA_ARGS__ }; } \ - void name(const type& value) noexcept { _##name = value; } \ - \ -private: \ +#define ACTION_ARG(type, name, ...) \ +public: \ + type name() const noexcept \ + { \ + return _##name.has_value() ? _##name.value() : type{ __VA_ARGS__ }; \ + } \ + void name(const type& value) noexcept \ + { \ + _##name = value; \ + } \ + \ +private: \ std::optional _##name{ std::nullopt }; // Notes on defining ActionArgs and ActionEventArgs: diff --git a/src/cascadia/TerminalSettingsModel/IInheritable.idl.h b/src/cascadia/TerminalSettingsModel/IInheritable.idl.h index b41822998e0..a6f6529e22e 100644 --- a/src/cascadia/TerminalSettingsModel/IInheritable.idl.h +++ b/src/cascadia/TerminalSettingsModel/IInheritable.idl.h @@ -9,7 +9,10 @@ get; \ set; \ }; \ - Boolean Has##Name { get; }; \ + Boolean Has##Name \ + { \ + get; \ + }; \ void Clear##Name() #define INHERITABLE_SETTING(Type, Name) \ diff --git a/src/cascadia/inc/cppwinrt_utils.h b/src/cascadia/inc/cppwinrt_utils.h index fd8aff723bc..142730610c1 100644 --- a/src/cascadia/inc/cppwinrt_utils.h +++ b/src/cascadia/inc/cppwinrt_utils.h @@ -32,9 +32,15 @@ protected: \ // Winrt events need a method for adding a callback to the event and removing // the callback. This macro will define them both for you, because they // don't really vary from event to event. -#define DEFINE_EVENT(className, name, eventHandler, args) \ - winrt::event_token className::name(const args& handler) { return eventHandler.add(handler); } \ - void className::name(const winrt::event_token& token) noexcept { eventHandler.remove(token); } +#define DEFINE_EVENT(className, name, eventHandler, args) \ + winrt::event_token className::name(const args& handler) \ + { \ + return eventHandler.add(handler); \ + } \ + void className::name(const winrt::event_token& token) noexcept \ + { \ + eventHandler.remove(token); \ + } // This is a helper macro to make declaring events easier. // This will declare the event handler and the methods for adding and removing a @@ -53,9 +59,15 @@ private: // the callback. This macro will define them both for you, because they // don't really vary from event to event. // Use this if you have a Windows.Foundation.TypedEventHandler -#define DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(className, name, eventHandler, sender, args) \ - winrt::event_token className::name(const Windows::Foundation::TypedEventHandler& handler) { return eventHandler.add(handler); } \ - void className::name(const winrt::event_token& token) noexcept { eventHandler.remove(token); } +#define DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(className, name, eventHandler, sender, args) \ + winrt::event_token className::name(const Windows::Foundation::TypedEventHandler& handler) \ + { \ + return eventHandler.add(handler); \ + } \ + void className::name(const winrt::event_token& token) noexcept \ + { \ + eventHandler.remove(token); \ + } // This is a helper macro for both declaring the signature of an event, and // defining the body. Winrt events need a method for adding a callback to the @@ -63,12 +75,18 @@ private: // signatures and define them both for you, because they don't really vary from // event to event. // Use this in a classes header if you have a Windows.Foundation.TypedEventHandler -#define TYPED_EVENT(name, sender, args) \ -public: \ - winrt::event_token name(const winrt::Windows::Foundation::TypedEventHandler& handler) { return _##name##Handlers.add(handler); } \ - void name(const winrt::event_token& token) { _##name##Handlers.remove(token); } \ - \ -private: \ +#define TYPED_EVENT(name, sender, args) \ +public: \ + winrt::event_token name(const winrt::Windows::Foundation::TypedEventHandler& handler) \ + { \ + return _##name##Handlers.add(handler); \ + } \ + void name(const winrt::event_token& token) \ + { \ + _##name##Handlers.remove(token); \ + } \ + \ +private: \ winrt::event> _##name##Handlers; // This is a helper macro for both declaring the signature of a callback (nee event) and @@ -77,12 +95,18 @@ private: // signatures and define them both for you, because they don't really vary from // event to event. // Use this in a class's header if you have a "delegate" type in your IDL. -#define WINRT_CALLBACK(name, args) \ -public: \ - winrt::event_token name(const args& handler) { return _##name##Handlers.add(handler); } \ - void name(const winrt::event_token& token) { _##name##Handlers.remove(token); } \ - \ -protected: \ +#define WINRT_CALLBACK(name, args) \ +public: \ + winrt::event_token name(const args& handler) \ + { \ + return _##name##Handlers.add(handler); \ + } \ + void name(const winrt::event_token& token) \ + { \ + _##name##Handlers.remove(token); \ + } \ + \ +protected: \ winrt::event _##name##Handlers; // This is a helper macro for both declaring the signature and body of an event @@ -91,16 +115,28 @@ protected: // "proxied" to the handling type. Case in point: many of the events on App are // just forwarded straight to TerminalPage. This macro will both declare the // method signatures and define them both for you. -#define FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ -public: \ - winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) { return handler->handlerName(h); } \ - void name(const winrt::event_token& token) noexcept { handler->handlerName(token); } +#define FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ +public: \ + winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) \ + { \ + return handler->handlerName(h); \ + } \ + void name(const winrt::event_token& token) noexcept \ + { \ + handler->handlerName(token); \ + } // Same thing, but handler is a projected type, not an implementation -#define PROJECTED_FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ -public: \ - winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) { return handler.handlerName(h); } \ - void name(const winrt::event_token& token) noexcept { handler.handlerName(token); } +#define PROJECTED_FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ +public: \ + winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) \ + { \ + return handler.handlerName(h); \ + } \ + void name(const winrt::event_token& token) noexcept \ + { \ + handler.handlerName(token); \ + } // This is a bit like *FORWARDED_TYPED_EVENT. When you use a forwarded event, // the handler gets added to the object that's raising the event. For example, @@ -121,17 +157,26 @@ public: // _core.TitleChanged({ get_weak(), &TermControl::_bubbleTitleChanged }); #define BUBBLED_FORWARDED_TYPED_EVENT(name, sender, args) \ TYPED_EVENT(name, sender, args) \ - void _bubble##name(const sender& s, const args& a) { _##name##Handlers(s, a); } + void _bubble##name(const sender& s, const args& a) \ + { \ + _##name##Handlers(s, a); \ + } // Use this macro to quick implement both the getter and setter for a property. // This should only be used for simple types where there's no logic in the // getter/setter beyond just accessing/updating the value. -#define WINRT_PROPERTY(type, name, ...) \ -public: \ - type name() const noexcept { return _##name; } \ - void name(const type& value) noexcept { _##name = value; } \ - \ -private: \ +#define WINRT_PROPERTY(type, name, ...) \ +public: \ + type name() const noexcept \ + { \ + return _##name; \ + } \ + void name(const type& value) noexcept \ + { \ + _##name = value; \ + } \ + \ +private: \ type _##name{ __VA_ARGS__ }; // Use this macro to quickly implement both the getter and setter for an @@ -143,7 +188,10 @@ private: \ // (like when the class is being initialized). #define WINRT_OBSERVABLE_PROPERTY(type, name, event, ...) \ public: \ - type name() const noexcept { return _##name; }; \ + type name() const noexcept \ + { \ + return _##name; \ + }; \ void name(const type& value) \ { \ if (_##name != value) \ diff --git a/src/inc/til/bytes.h b/src/inc/til/bytes.h index 593a0cd435f..f2ab2c6e22d 100644 --- a/src/inc/til/bytes.h +++ b/src/inc/til/bytes.h @@ -40,7 +40,7 @@ namespace til } template - requires TriviallyCopyable + requires TriviallyCopyable constexpr void bytes_transfer(Target& target, Source& source) { using TargetType = typename Target::value_type; diff --git a/src/inc/til/pmr.h b/src/inc/til/pmr.h index a900449c5e5..71fc596e316 100644 --- a/src/inc/til/pmr.h +++ b/src/inc/til/pmr.h @@ -39,7 +39,7 @@ namespace til::pmr { if (align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { - return ::operator new (bytes, std::align_val_t{ align }); + return ::operator new(bytes, std::align_val_t{ align }); } return ::operator new(bytes); @@ -49,7 +49,7 @@ namespace til::pmr { if (align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { - return ::operator delete (ptr, bytes, std::align_val_t{ align }); + return ::operator delete(ptr, bytes, std::align_val_t{ align }); } ::operator delete(ptr, bytes); diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 146d8053d1d..1ca8f150415 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -94,14 +94,35 @@ namespace Microsoft::Console::Render return __builtin_memcmp(this, &rhs, sizeof(rhs)) != 0; \ } -#define ATLAS_FLAG_OPS(type, underlying) \ - friend constexpr type operator~(type v) noexcept { return static_cast(~static_cast(v)); } \ - friend constexpr type operator|(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) | static_cast(rhs)); } \ - friend constexpr type operator&(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) & static_cast(rhs)); } \ - friend constexpr type operator^(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) ^ static_cast(rhs)); } \ - friend constexpr void operator|=(type& lhs, type rhs) noexcept { lhs = lhs | rhs; } \ - friend constexpr void operator&=(type& lhs, type rhs) noexcept { lhs = lhs & rhs; } \ - friend constexpr void operator^=(type& lhs, type rhs) noexcept { lhs = lhs ^ rhs; } +#define ATLAS_FLAG_OPS(type, underlying) \ + friend constexpr type operator~(type v) noexcept \ + { \ + return static_cast(~static_cast(v)); \ + } \ + friend constexpr type operator|(type lhs, type rhs) noexcept \ + { \ + return static_cast(static_cast(lhs) | static_cast(rhs)); \ + } \ + friend constexpr type operator&(type lhs, type rhs) noexcept \ + { \ + return static_cast(static_cast(lhs) & static_cast(rhs)); \ + } \ + friend constexpr type operator^(type lhs, type rhs) noexcept \ + { \ + return static_cast(static_cast(lhs) ^ static_cast(rhs)); \ + } \ + friend constexpr void operator|=(type& lhs, type rhs) noexcept \ + { \ + lhs = lhs | rhs; \ + } \ + friend constexpr void operator&=(type& lhs, type rhs) noexcept \ + { \ + lhs = lhs & rhs; \ + } \ + friend constexpr void operator^=(type& lhs, type rhs) noexcept \ + { \ + lhs = lhs ^ rhs; \ + } template struct vec2 diff --git a/tools/packages.config b/tools/packages.config index f52e4f8eab2..035bd89d02c 100644 --- a/tools/packages.config +++ b/tools/packages.config @@ -1,4 +1,4 @@ - + From 5f709204912cf9aede6172e01d2f0ea1aa5e34d1 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 5 Apr 2023 17:04:58 -0500 Subject: [PATCH 016/226] When unpackaged, isolate the monarch by the install path (#15118) Unpackaged installations don't have the luxury of magic package isolation to stop them from accidentally touching each other's monarchs. We need to enforce that ourselves by making their monarch CLSIDs unique per install. We'll use a v5 UUID based on the install folder to unique them. Closes #15117 --- src/cascadia/Remoting/WindowManager.cpp | 35 +++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 6331e3e9eb8..f53928398cf 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -15,11 +15,42 @@ #include "WindowManager.g.cpp" #include "../../types/inc/utils.hpp" +#include + using namespace winrt; using namespace winrt::Microsoft::Terminal; using namespace winrt::Windows::Foundation; using namespace ::Microsoft::Console; +namespace +{ + const GUID& MonarchCLSID() + { + if (!IsPackaged()) [[unlikely]] + { + // Unpackaged installations don't have the luxury of magic package isolation + // to stop them from accidentally touching each other's monarchs. + // We need to enforce that ourselves by making their monarch CLSIDs unique + // per install. + // This applies in both portable mode and normal unpackaged mode. + // We'll use a v5 UUID based on the install folder to unique them. + static GUID processRootHashedGuid = []() { + // {5456C4DB-557D-4A22-B043-B1577418E4AF} + static constexpr GUID processRootHashedGuidBase = { 0x5456c4db, 0x557d, 0x4a22, { 0xb0, 0x43, 0xb1, 0x57, 0x74, 0x18, 0xe4, 0xaf } }; + + // Make a temporary monarch CLSID based on the unpackaged install root + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; + modulePath.remove_filename(); + std::wstring pathRootAsString{ modulePath.wstring() }; + + return Utils::CreateV5Uuid(processRootHashedGuidBase, std::as_bytes(std::span{ pathRootAsString })); + }(); + return processRootHashedGuid; + } + return Monarch_clsid; + } +} + namespace winrt::Microsoft::Terminal::Remoting::implementation { WindowManager::WindowManager() @@ -45,7 +76,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // // * If we're running unpackaged: the .winmd must be a sibling of the .exe // * If we're running packaged: the .winmd must be in the package root - _monarch = try_create_instance(Monarch_clsid, + _monarch = try_create_instance(MonarchCLSID(), CLSCTX_LOCAL_SERVER); } @@ -69,7 +100,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void WindowManager::_registerAsMonarch() { - winrt::check_hresult(CoRegisterClassObject(Monarch_clsid, + winrt::check_hresult(CoRegisterClassObject(MonarchCLSID(), winrt::make<::MonarchFactory>().get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, From 5db8af62775b1a12ae7888e84d5f2296f86239f4 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 6 Apr 2023 00:52:04 +0200 Subject: [PATCH 017/226] Return success if ReadCharacterInput read >0 characters (#15122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a regression caused by 599b550. If I'm reading `stream.cpp` in cf87590 right, it returns `STATUS_SUCCESS` if `ReadCharacterInput` read at least 1 character. I think? this PR makes the code behave exactly equivalent. The old code is a bit of an "acquired taste" so it's a bit hard to tell. Closes #15116 ## PR Checklist * Run `more long_text_file.txt` in cmd * Press Spacebar * Scrolls down ✅ * Press Q * Exits ✅ --- src/host/stream.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/host/stream.cpp b/src/host/stream.cpp index 5ab812bda9a..9ecccd2fadb 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -436,16 +436,16 @@ try inputBuffer.ConsumeCached(unicode, writer); - // We don't need to wait for input if `ConsumeCached` read something already, which is - // indicated by the writer having been advanced (= it's shorter than the original buffer). - auto wait = writer.size() == buffer.size(); + auto noDataReadYet = writer.size() == buffer.size(); auto status = STATUS_SUCCESS; while (writer.size() >= charSize) { wchar_t wch; - status = GetChar(&inputBuffer, &wch, wait, nullptr, nullptr, nullptr); - if (!NT_SUCCESS(status)) + // We don't need to wait for input if `ConsumeCached` read something already, which is + // indicated by the writer having been advanced (= it's shorter than the original buffer). + status = GetChar(&inputBuffer, &wch, noDataReadYet, nullptr, nullptr, nullptr); + if (FAILED_NTSTATUS(status)) { break; } @@ -453,11 +453,13 @@ try std::wstring_view wchView{ &wch, 1 }; inputBuffer.Consume(unicode, wchView, writer); - wait = false; + noDataReadYet = false; } bytesRead = buffer.size() - writer.size(); - return status; + // Once we read some data off the InputBuffer it can't be read again, so we + // need to make sure to return a success status to the client in that case. + return noDataReadYet ? status : STATUS_SUCCESS; } NT_CATCH_RETURN() From a98a0cf2c696395cbdd71239f32263a6b65e7c97 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 5 Apr 2023 18:10:54 -0500 Subject: [PATCH 018/226] Stop the beef when you hover off a hyperlink (#15120) Big surprise, apparently W.F.Uri can parse the empty string into garbage! --- src/cascadia/TerminalControl/TermControl.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index d70b5c2e937..12c7ca25416 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3017,20 +3017,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (lastHoveredCell) { winrt::hstring uriText = _core.HoveredUriText(); + if (uriText.empty()) + { + co_return; + } + try { // DisplayUri will filter out non-printable characters and confusables. Windows::Foundation::Uri parsedUri{ uriText }; + if (!parsedUri) + { + co_return; + } uriText = parsedUri.DisplayUri(); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - uriText = {}; - } - if (!uriText.empty()) - { const auto panel = SwapChainPanel(); const auto scale = panel.CompositionScaleX(); const auto offset = panel.ActualOffset(); @@ -3052,6 +3053,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), locationInDIPs.x - offset.x); OverlayCanvas().SetTop(HyperlinkTooltipBorder(), locationInDIPs.y - offset.y); } + CATCH_LOG(); } } } From e73362d45b26e8b6578132d78abaab2519f2de70 Mon Sep 17 00:00:00 2001 From: michalnpl Date: Thu, 6 Apr 2023 08:51:57 -0700 Subject: [PATCH 019/226] Respect the codepage stored in .LNK files in conhost (#15111) Making Conhost pick up codepage from .lnk files. Because of the wrong assignment order, the Conhost was not picking up the codepage stored in .lnk shortcut files. This change fixes this issue by changing the order of the assignment to the correct one. This is a potential backward compatibility issue. Since this issue has been present in the codebase for years, this change runs a high risk of breaking backward compatibility with software that depends on incorrect behavior. ## Validation Steps Performed Tested fix manually (using chcp command, making sure each .lnk codepage was picked up.) against Debug/Release x64 builds with 5 different .lnk files: 1. Arabic codepage 1256 2. Greek 869 3. Latin2 852 4. Thai 874 5. Traditional Chinese 50229 Ran TAEF tests against Debug/Release x64/x86 with identical results as main branch. Tested against invalid codepage numbers by manually manipulating .lnk file binary. In case of an invalid codepage number, Conhost defaults to a valid default one, which I assume is expected behavior. Closes #14942 --- src/propslib/ShortcutSerialization.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/propslib/ShortcutSerialization.cpp b/src/propslib/ShortcutSerialization.cpp index 8c27834eeed..e930b25af83 100644 --- a/src/propslib/ShortcutSerialization.cpp +++ b/src/propslib/ShortcutSerialization.cpp @@ -165,7 +165,7 @@ void ShortcutSerialization::s_SetLinkPropertyDwordValue(_Inout_ IPropertyStore* NT_FE_CONSOLE_PROPS* pNtFEConsoleProps; if (SUCCEEDED(pConsoleLnkDataList->CopyDataBlock(NT_FE_CONSOLE_PROPS_SIG, reinterpret_cast(&pNtFEConsoleProps)))) { - pNtFEConsoleProps->uCodePage = pStateInfo->CodePage; + pStateInfo->CodePage = pNtFEConsoleProps->uCodePage; LocalFree(pNtFEConsoleProps); } } From fe66ba5f58b02c8da523db19d48f13d9ebe4b4e4 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 6 Apr 2023 11:50:30 -0500 Subject: [PATCH 020/226] Add "legacy" themes (#15108) This is a minimal version of the requests for #14858. In that thread we discussed FULL reverting the default themes to the old ones. In later discussion, we decided _meh_, let's just ship the legacy themes too, so it's easy to go back if you should choose. The default still remains the sane `dark`, but the `legacy*` themes are all right there, and given the same special treatment as the other inbox themes. Closes #14858 Closes #14844 --- .../GlobalAppearanceViewModel.cpp | 15 ++++++++++ .../Resources/en-US/Resources.resw | 18 +++++++++-- .../CascadiaSettingsSerialization.cpp | 20 +++++++++---- .../TerminalSettingsModel/defaults.json | 30 +++++++++++++++++++ 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp index c9e9c51ec73..27c5abbec8a 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp @@ -43,6 +43,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation constexpr std::wstring_view systemThemeName{ L"system" }; constexpr std::wstring_view darkThemeName{ L"dark" }; constexpr std::wstring_view lightThemeName{ L"light" }; + constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" }; + constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" }; + constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" }; GlobalAppearanceViewModel::GlobalAppearanceViewModel(Model::GlobalAppSettings globalSettings) : _GlobalSettings{ globalSettings }, @@ -248,6 +251,18 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { return RS_(L"Globals_ThemeSystem/Content"); } + else if (theme.Name() == legacyDarkThemeName) + { + return RS_(L"Globals_ThemeDarkLegacy/Content"); + } + else if (theme.Name() == legacyLightThemeName) + { + return RS_(L"Globals_ThemeLightLegacy/Content"); + } + else if (theme.Name() == legacySystemThemeName) + { + return RS_(L"Globals_ThemeSystemLegacy/Content"); + } return theme.Name(); } diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 3c53d1ee4f3..c66785fb200 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -536,15 +536,27 @@ Dark - An option to choose from for the "tab width mode" setting. When selected, the app is in dark theme and darker colors are used throughout the app. + An option to choose from for the "theme" setting. When selected, the app is in dark theme and darker colors are used throughout the app. Use Windows theme - An option to choose from for the "tab width mode" setting. When selected, the app uses the theme selected in the Windows operating system. + An option to choose from for the "theme" setting. When selected, the app uses the theme selected in the Windows operating system. Light - An option to choose from for the "tab width mode" setting. When selected, the app is in light theme and lighter colors are used throughout the app. + An option to choose from for the "theme" setting. When selected, the app is in light theme and lighter colors are used throughout the app. + + + Dark (Legacy) + An option to choose from for the "theme" setting. When selected, the app is in dark theme and darker colors are used throughout the app. This is an older version of the "dark" theme + + + Use Windows theme (Legacy) + An option to choose from for the "theme" setting. When selected, the app uses the theme selected in the Windows operating system. This is an older version of the "Use Windows theme" theme + + + Light (Legacy) + An option to choose from for the "theme" setting. When selected, the app is in light theme and lighter colors are used throughout the app. This is an older version of the "light" theme Word delimiters diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index f80f5a627f8..9b73d35565e 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -52,6 +52,9 @@ static constexpr std::string_view ThemesKey{ "themes" }; constexpr std::wstring_view systemThemeName{ L"system" }; constexpr std::wstring_view darkThemeName{ L"dark" }; constexpr std::wstring_view lightThemeName{ L"light" }; +constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" }; +constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" }; +constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" }; static constexpr std::wstring_view jsonExtension{ L".json" }; static constexpr std::wstring_view FragmentsSubDirectory{ L"\\Fragments" }; @@ -562,8 +565,9 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source { if (const auto theme = Theme::FromJson(themeJson)) { + const auto& name{ theme->Name() }; if (origin != OriginTag::InBox && - (theme->Name() == systemThemeName || theme->Name() == lightThemeName || theme->Name() == darkThemeName)) + (name == systemThemeName || name == lightThemeName || name == darkThemeName || name == legacySystemThemeName || name == legacyDarkThemeName || name == legacyLightThemeName)) { // If the theme didn't come from the in-box themes, and its // name was one of the reserved names, then just ignore it. @@ -953,10 +957,16 @@ void CascadiaSettings::_researchOnLoad() // light: 1 // dark: 2 // a custom theme: 3 - const auto themeChoice = themeInUse == L"system" ? 0 : - themeInUse == L"light" ? 1 : - themeInUse == L"dark" ? 2 : - 3; + // system (legacy): 4 + // light (legacy): 5 + // dark (legacy): 6 + const auto themeChoice = themeInUse == L"system" ? 0 : + themeInUse == L"light" ? 1 : + themeInUse == L"dark" ? 2 : + themeInUse == L"legacyDark" ? 4 : + themeInUse == L"legacyLight" ? 5 : + themeInUse == L"legacySystem" ? 6 : + 3; TraceLoggingWrite( g_hSettingsModelProvider, diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 9d850dd1c9b..c57f6320c6c 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -316,6 +316,36 @@ "background": "terminalBackground", "unfocusedBackground": "#00000000" } + }, + { + "name": "legacyDark", + "tab": { + "background": null, + "unfocusedBackground": null + }, + "window": { + "applicationTheme": "dark" + } + }, + { + "name": "legacyLight", + "tab": { + "background": null, + "unfocusedBackground": null + }, + "window": { + "applicationTheme": "light" + } + }, + { + "name": "legacySystem", + "tab": { + "background": null, + "unfocusedBackground": null + }, + "window": { + "applicationTheme": "system" + }, } ], "actions": From 6f8ef58673c77c84a23ece4da47176321ca256cf Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 6 Apr 2023 12:39:40 -0500 Subject: [PATCH 021/226] Add support for running the Terminal without _any_ windows (#14944) This adds a setting (`compatibility.allowHeadless`) to let the Terminal keep running even when all windows are closed. This lets hotkeys keep working, because the Emperor thread is still running, just, without any windows. I'm really tempted to invoke the magic "closes" word on #9996, but honestly, we should also add some sort of support for `wt --headless` or `wt --hidden` or whatever, before we close that. There's also #13630 which seems imminently doable. Tested manually. I'd post a gif of "close all terminal windows, then invoke the quakeMode binding and \*presto\*, but that would be an unnecessarily big gif. Related to #9996 but not enough to close it if you ask me --- src/cascadia/TerminalApp/AppLogic.cpp | 10 ++++++++++ src/cascadia/TerminalApp/AppLogic.h | 1 + src/cascadia/TerminalApp/AppLogic.idl | 1 + .../TerminalSettingsModel/GlobalAppSettings.idl | 1 + src/cascadia/TerminalSettingsModel/MTSMSettings.h | 1 + src/cascadia/WindowsTerminal/WindowEmperor.cpp | 10 +++++++++- src/cascadia/WindowsTerminal/WindowEmperor.h | 2 ++ 7 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 2f7a49ef0ab..6657efb67ca 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -703,6 +703,16 @@ namespace winrt::TerminalApp::implementation globals.MinimizeToNotificationArea(); } + bool AppLogic::AllowHeadless() + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + ReloadSettings(); + } + return _settings.GlobalSettings().AllowHeadless(); + } + TerminalApp::TerminalWindow AppLogic::CreateNewWindow() { if (_settings == nullptr) diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index f5a206b1826..c3bbf12f5fe 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -67,6 +67,7 @@ namespace winrt::TerminalApp::implementation Microsoft::Terminal::Settings::Model::Theme Theme(); bool IsolatedMode(); + bool AllowHeadless(); bool RequestsTrayIcon(); TerminalApp::TerminalWindow CreateNewWindow(); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 5eac395390f..cb5c920bbee 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -46,6 +46,7 @@ namespace TerminalApp // Selected settings to expose Microsoft.Terminal.Settings.Model.Theme Theme { get; }; Boolean IsolatedMode { get; }; + Boolean AllowHeadless { get; }; Boolean RequestsTrayIcon { get; }; FindTargetWindowResult FindTargetWindow(String[] args); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 5a32d3eebf0..a9745cf7804 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -99,6 +99,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(IVector, NewTabMenu); INHERITABLE_SETTING(Boolean, EnableColorSelection); INHERITABLE_SETTING(Boolean, IsolatedMode); + INHERITABLE_SETTING(Boolean, AllowHeadless); Windows.Foundation.Collections.IMapView ColorSchemes(); void AddColorScheme(ColorScheme scheme); diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 798505de23d..84a6002d0ee 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -64,6 +64,7 @@ Author(s): X(bool, TrimPaste, "trimPaste", true) \ X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \ X(winrt::Windows::Foundation::Collections::IVector, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} })) \ + X(bool, AllowHeadless, "compatibility.allowHeadless", false) \ X(bool, IsolatedMode, "compatibility.isolatedMode", false) #define MTSM_PROFILE_SETTINGS(X) \ diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 3343004aa0b..52b688485ce 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -202,7 +202,13 @@ void WindowEmperor::_windowExitedHandler(uint64_t senderID) return w->Peasant().GetID() == senderID; }); - if (_windowThreadInstances.fetch_sub(1, std::memory_order_relaxed) == 1) + // When we run out of windows, exit our process if and only if: + // * We're not allowed to run headless OR + // * we've explicitly been told to "quit", which should fully exit the Terminal. + const bool quitWhenLastWindowExits{ !_app.Logic().AllowHeadless() }; + const bool noMoreWindows{ _windowThreadInstances.fetch_sub(1, std::memory_order_relaxed) == 1 }; + if (noMoreWindows && + (_quitting || quitWhenLastWindowExits)) { _close(); } @@ -297,6 +303,8 @@ void WindowEmperor::_numberOfWindowsChanged(const winrt::Windows::Foundation::II void WindowEmperor::_quitAllRequested(const winrt::Windows::Foundation::IInspectable&, const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) { + _quitting = true; + // Make sure that the current timer is destroyed so that it doesn't attempt // to run while we are in the middle of quitting. if (_getWindowLayoutThrottler.has_value()) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 73e3f1ebbe1..200546c21ba 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -52,6 +52,8 @@ class WindowEmperor : public std::enable_shared_from_this std::unique_ptr _notificationIcon; + bool _quitting{ false }; + void _windowStartedHandlerPostXAML(const std::shared_ptr& sender); void _windowExitedHandler(uint64_t senderID); From 083fc647bb1c59bacf2906504780fe7993aa6cd1 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 6 Apr 2023 13:03:25 -0500 Subject: [PATCH 022/226] Revert the revert of "Hide the window until we're finished with initialization" (#13811) This is a revert of the revert of #12979. We mainly reverted that PR because of an issue where restored windows would grow/shrink slightly on external displays. It was too close to the ship date for that release, so we backed it out wholesale (in #13098). I think I've found the real root of the problem, and fixed it here. The money diff here from the original PR: 4c08b9a1bc2e90b8284e4d8117d0de400784520f...c34495dcfc19ea67a9f4f9673d422760200683ab. Basically, I had put the part where we actually handle the creation of the window into `_AppInitializedHandler`, when we should have left the window to be created in `_HandleCreateWindow` We create it there, _hidden_, and then should only _show_ it in `_AppInitializedHandler`. I'm _NOT_ incorporating the change for #9053. I reverted that bit in 1fac40355. I am too worried about that messing with the phwnd that I wanted to get that reviewed and committed atomically, separately. * fixes #11561 * tested manually * I work here --- src/cascadia/TerminalApp/TerminalPage.cpp | 33 ++++++++- src/cascadia/TerminalApp/TerminalPage.h | 4 +- src/cascadia/TerminalApp/TerminalPage.idl | 2 +- src/cascadia/TerminalApp/TerminalWindow.h | 2 + src/cascadia/TerminalApp/TerminalWindow.idl | 2 + src/cascadia/WindowsTerminal/AppHost.cpp | 69 +++++++++++++++++-- src/cascadia/WindowsTerminal/AppHost.h | 11 ++- src/cascadia/WindowsTerminal/IslandWindow.cpp | 14 ++-- src/cascadia/WindowsTerminal/IslandWindow.h | 5 +- .../WindowsTerminal/WindowEmperor.cpp | 15 ++-- 10 files changed, 121 insertions(+), 36 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 9b0abe8904a..d2545d0b3ab 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -623,7 +623,7 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - void TerminalPage::_CompleteInitialization() + winrt::fire_and_forget TerminalPage::_CompleteInitialization() { _startupState = StartupState::Initialized; @@ -643,10 +643,39 @@ namespace winrt::TerminalApp::implementation if (_tabs.Size() == 0 && !(_shouldStartInboundListener || _isEmbeddingInboundListener)) { _LastTabClosedHandlers(*this, winrt::make(false)); + co_return; } else { - _InitializedHandlers(*this, nullptr); + // GH#11561: When we start up, our window is initially just a frame + // with a transparent content area. We're gonna do all this startup + // init on the UI thread, so the UI won't actually paint till it's + // all done. This results in a few frames where the frame is + // visible, before the page paints for the first time, before any + // tabs appears, etc. + // + // To mitigate this, we're gonna wait for the UI thread to finish + // everything it's gotta do for the initial init, and _then_ fire + // our Initialized event. By waiting for everything else to finish + // (CoreDispatcherPriority::Low), we let all the tabs and panes + // actually get created. In the window layer, we're gonna cloak the + // window till this event is fired, so we don't actually see this + // frame until we're actually all ready to go. + // + // This will result in the window seemingly not loading as fast, but + // it will actually take exactly the same amount of time before it's + // usable. + // + // We also experimented with drawing a solid BG color before the + // initialization is finished. However, there are still a few frames + // after the frame is displayed before the XAML content first draws, + // so that didn't actually resolve any issues. + Dispatcher().RunAsync(CoreDispatcherPriority::Low, [weak = get_weak()]() { + if (auto self{ weak.get() }) + { + self->_InitializedHandlers(*self, nullptr); + } + }); } } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index b178389ec2b..423ffea1b36 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -180,7 +180,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(AlwaysOnTopChanged, IInspectable, IInspectable); TYPED_EVENT(RaiseVisualBell, IInspectable, IInspectable); TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable); - TYPED_EVENT(Initialized, IInspectable, winrt::Windows::UI::Xaml::RoutedEventArgs); + TYPED_EVENT(Initialized, IInspectable, IInspectable); TYPED_EVENT(IdentifyWindowsRequested, IInspectable, IInspectable); TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs); TYPED_EVENT(SummonWindowRequested, IInspectable, IInspectable); @@ -447,7 +447,7 @@ namespace winrt::TerminalApp::implementation void _StartInboundListener(); - void _CompleteInitialization(); + winrt::fire_and_forget _CompleteInitialization(); void _FocusActiveControl(IInspectable sender, IInspectable eventArgs); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 5c8a151b909..92052ed1677 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -89,7 +89,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler FocusModeChanged; event Windows.Foundation.TypedEventHandler FullscreenChanged; event Windows.Foundation.TypedEventHandler AlwaysOnTopChanged; - event Windows.Foundation.TypedEventHandler Initialized; + event Windows.Foundation.TypedEventHandler Initialized; event Windows.Foundation.TypedEventHandler SetTaskbarProgress; event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler RenameWindowRequested; diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index adb2c9b6638..5c923e25df6 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -203,6 +203,8 @@ namespace winrt::TerminalApp::implementation // These are events that are handled by the TerminalPage, but are // exposed through the AppLogic. This macro is used to forward the event // directly to them. + FORWARDED_TYPED_EVENT(Initialized, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, Initialized); + FORWARDED_TYPED_EVENT(SetTitleBarContent, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::UIElement, _root, SetTitleBarContent); FORWARDED_TYPED_EVENT(TitleChanged, winrt::Windows::Foundation::IInspectable, winrt::hstring, _root, TitleChanged); FORWARDED_TYPED_EVENT(LastTabClosed, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::LastTabClosedEventArgs, _root, LastTabClosed); diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index 825afa44c4a..912663e4a0b 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -114,6 +114,8 @@ namespace TerminalApp // information. void DismissDialog(); + event Windows.Foundation.TypedEventHandler Initialized; + event Windows.Foundation.TypedEventHandler SetTitleBarContent; event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler LastTabClosed; diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 2c5ba7ab153..633b3519e50 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -60,8 +60,7 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, auto pfn = std::bind(&AppHost::_HandleCreateWindow, this, std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3); + std::placeholders::_2); _window->SetCreateCallback(pfn); // Event handlers: @@ -339,6 +338,7 @@ void AppHost::Initialize() _window->UpdateSettingsRequested({ this, &AppHost::_requestUpdateSettings }); + _revokers.Initialized = _windowLogic.Initialized(winrt::auto_revoke, { this, &AppHost::_WindowInitializedHandler }); _revokers.RequestedThemeChanged = _windowLogic.RequestedThemeChanged(winrt::auto_revoke, { this, &AppHost::_UpdateTheme }); _revokers.FullscreenChanged = _windowLogic.FullscreenChanged(winrt::auto_revoke, { this, &AppHost::_FullscreenChanged }); _revokers.FocusModeChanged = _windowLogic.FocusModeChanged(winrt::auto_revoke, { this, &AppHost::_FocusModeChanged }); @@ -510,12 +510,31 @@ LaunchPosition AppHost::_GetWindowLaunchPosition() return pos; } +// Method Description: +// - Callback for when the window is first being created (during WM_CREATE). +// Stash the proposed size for later. We'll need that once we're totally +// initialized, so that we can show the window in the right position *when we +// want to show it*. If we did the _initialResizeAndRepositionWindow work now, +// it would have no effect, because the window is not yet visible. +// Arguments: +// - hwnd: The HWND of the window we're about to create. +// - proposedRect: The location and size of the window that we're about to +// create. We'll use this rect to determine which monitor the window is about +// to appear on. +void AppHost::_HandleCreateWindow(const HWND /* hwnd */, const til::rect& proposedRect) +{ + // GH#11561: Hide the window until we're totally done being initialized. + // More commentary in TerminalPage::_CompleteInitialization + _initialResizeAndRepositionWindow(_window->GetHandle(), proposedRect, _launchMode); +} + // Method Description: // - Resize the window we're about to create to the appropriate dimensions, as -// specified in the settings. This will be called during the handling of -// WM_CREATE. We'll load the settings for the app, then get the proposed size -// of the terminal from the app. Using that proposed size, we'll resize the -// window we're creating, so that it'll match the values in the settings. +// specified in the settings. This is called once the app has finished it's +// initial setup, once we have created all the tabs, panes, etc. We'll load +// the settings for the app, then get the proposed size of the terminal from +// the app. Using that proposed size, we'll resize the window we're creating, +// so that it'll match the values in the settings. // Arguments: // - hwnd: The HWND of the window we're about to create. // - proposedRect: The location and size of the window that we're about to @@ -524,7 +543,7 @@ LaunchPosition AppHost::_GetWindowLaunchPosition() // - launchMode: A LaunchMode enum reference that indicates the launch mode // Return Value: // - None -void AppHost::_HandleCreateWindow(const HWND hwnd, til::rect proposedRect, LaunchMode& launchMode) +void AppHost::_initialResizeAndRepositionWindow(const HWND hwnd, til::rect proposedRect, LaunchMode& launchMode) { launchMode = _windowLogic.GetLaunchMode(); @@ -1211,6 +1230,42 @@ void AppHost::_PropertyChangedHandler(const winrt::Windows::Foundation::IInspect } } +winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*arg*/) +{ + // GH#11561: We're totally done being initialized. Resize the window to + // match the initial settings, and then call ShowWindow to finally make us + // visible. + + auto nCmdShow = SW_SHOW; + if (WI_IsFlagSet(_launchMode, LaunchMode::MaximizedMode)) + { + nCmdShow = SW_MAXIMIZE; + } + + // For inexplicable reasons, again, hop to the BG thread, then back to the + // UI thread. This is shockingly load bearing - without this, then + // sometimes, we'll _still_ show the HWND before the XAML island actually + // paints. + co_await winrt::resume_background(); + co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher(), winrt::Windows::UI::Core::CoreDispatcherPriority::Low); + ShowWindow(_window->GetHandle(), nCmdShow); + + // If we didn't start the window hidden (in one way or another), then try to + // pull ourselves to the foreground. Don't necessarily do a whole "summon", + // we don't really want to STEAL foreground if someone rightfully took it + + const bool noForeground = nCmdShow == SW_SHOWMINIMIZED || + nCmdShow == SW_SHOWNOACTIVATE || + nCmdShow == SW_SHOWMINNOACTIVE || + nCmdShow == SW_SHOWNA || + nCmdShow == SW_FORCEMINIMIZE; + if (!noForeground) + { + SetForegroundWindow(_window->GetHandle()); + } +} + winrt::TerminalApp::TerminalWindow AppHost::Logic() { return _windowLogic; diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index dc2c3da91cc..6fa0c5c2191 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -39,6 +39,7 @@ class AppHost winrt::com_ptr _desktopManager{ nullptr }; bool _useNonClientArea{ false }; + winrt::Microsoft::Terminal::Settings::Model::LaunchMode _launchMode{}; std::shared_ptr> _showHideWindowThrottler; @@ -47,7 +48,8 @@ class AppHost void _HandleCommandlineArgs(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); - void _HandleCreateWindow(const HWND hwnd, til::rect proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); + void _HandleCreateWindow(const HWND hwnd, const til::rect& proposedRect); + void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::UIElement& arg); void _UpdateTheme(const winrt::Windows::Foundation::IInspectable&, @@ -60,6 +62,9 @@ class AppHost const winrt::Windows::Foundation::IInspectable& arg); void _AlwaysOnTopChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& arg); + winrt::fire_and_forget _WindowInitializedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& arg); + void _RaiseVisualBell(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& arg); void _WindowMouseWheeled(const til::point coord, const int32_t delta); @@ -116,7 +121,7 @@ class AppHost void _PropertyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); - void _initialResizeAndRepositionWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); + void _initialResizeAndRepositionWindow(const HWND hwnd, til::rect proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); void _handleMoveContent(const winrt::Windows::Foundation::IInspectable& sender, winrt::TerminalApp::RequestMoveContentArgs args); @@ -146,8 +151,10 @@ class AppHost winrt::Microsoft::Terminal::Remoting::Peasant::SummonRequested_revoker peasantSummonRequested; winrt::Microsoft::Terminal::Remoting::Peasant::DisplayWindowIdRequested_revoker peasantDisplayWindowIdRequested; winrt::Microsoft::Terminal::Remoting::Peasant::QuitRequested_revoker peasantQuitRequested; + winrt::Microsoft::Terminal::Remoting::Peasant::AttachRequested_revoker AttachRequested; + winrt::TerminalApp::TerminalWindow::Initialized_revoker Initialized; winrt::TerminalApp::TerminalWindow::CloseRequested_revoker CloseRequested; winrt::TerminalApp::TerminalWindow::RequestedThemeChanged_revoker RequestedThemeChanged; winrt::TerminalApp::TerminalWindow::FullscreenChanged_revoker FullscreenChanged; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 978159b2803..7e4c5ff8aee 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -111,7 +111,7 @@ void IslandWindow::Close() // window. // Return Value: // - -void IslandWindow::SetCreateCallback(std::function pfn) noexcept +void IslandWindow::SetCreateCallback(std::function pfn) noexcept { _pfnCreateCallback = pfn; } @@ -153,19 +153,13 @@ void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexce rc.right = rc.left + pcs->cx; rc.bottom = rc.top + pcs->cy; - auto launchMode = LaunchMode::DefaultMode; if (_pfnCreateCallback) { - _pfnCreateCallback(_window.get(), rc, launchMode); + _pfnCreateCallback(_window.get(), rc); } - auto nCmdShow = SW_SHOW; - if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode)) - { - nCmdShow = SW_MAXIMIZE; - } - - ShowWindow(_window.get(), nCmdShow); + // GH#11561: DO NOT call ShowWindow here. The AppHost will call ShowWindow + // once the app has completed its initialization. UpdateWindow(_window.get()); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index eb412a0f445..3673ce8f5b0 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -39,7 +39,8 @@ class IslandWindow : virtual void Initialize(); - void SetCreateCallback(std::function pfn) noexcept; + void SetCreateCallback(std::function pfn) noexcept; + void SetSnapDimensionCallback(std::function pfn) noexcept; void FocusModeChanged(const bool focusMode); @@ -97,7 +98,7 @@ class IslandWindow : winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; wil::com_ptr _taskbar; - std::function _pfnCreateCallback; + std::function _pfnCreateCallback; std::function _pfnSnapDimensionCallback; void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 52b688485ce..1284bd80370 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -169,18 +169,13 @@ void WindowEmperor::_windowStartedHandlerPostXAML(const std::shared_ptrLogic().IsQuakeWindowChanged({ this, &WindowEmperor::_windowIsQuakeWindowChanged }); sender->UpdateSettingsRequested({ this, &WindowEmperor::_windowRequestUpdateSettings }); - // Summon the window to the foreground, since we might not _currently_ be in + // DON'T Summon the window to the foreground, since we might not _currently_ be in // the foreground, but we should act like the new window is. // - // TODO: GH#14957 - use AllowSetForeground from the original wt.exe instead - Remoting::SummonWindowSelectionArgs args{}; - args.OnCurrentDesktop(false); - args.WindowID(sender->Peasant().GetID()); - args.SummonBehavior().MoveToCurrentDesktop(false); - args.SummonBehavior().ToggleVisibility(false); - args.SummonBehavior().DropdownDuration(0); - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); - _manager.SummonWindow(args); + // If you summon here, the resulting code will call ShowWindow(SW_SHOW) on + // the Terminal window, making it visible BEFORE the XAML island is actually + // ready to be drawn. We want to wait till the app's Initialized event + // before we make the window visible. // Now that the window is ready to go, we can add it to our list of windows, // because we know it will be well behaved. From c7498a426968c01022e521d5aedd8550e800c795 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 6 Apr 2023 15:01:00 -0500 Subject: [PATCH 023/226] PGO: Update the Helix payload to rely on the unpackaged distribution (#15123) The unpackaged distribution was made for this exact use, so let's *do it*! --- build/Helix/PrepareHelixPayload.ps1 | 15 ++++----------- .../Elements/TerminalApp.cs | 4 ++-- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/build/Helix/PrepareHelixPayload.ps1 b/build/Helix/PrepareHelixPayload.ps1 index 3f9600492e7..245744afc7f 100644 --- a/build/Helix/PrepareHelixPayload.ps1 +++ b/build/Helix/PrepareHelixPayload.ps1 @@ -55,14 +55,7 @@ Copy-Item "build\helix\runtests.cmd" $payloadDir Copy-Item "build\helix\InstallTestAppDependencies.ps1" "$payloadDir" Copy-Item "build\Helix\EnsureMachineState.ps1" "$payloadDir" -# Copy the APPX package from the 'drop' artifact dir and Windows Kits -Copy-Item "$repoDirectory\Artifacts\$ArtifactName\appx\CascadiaPackage_0.0.1.0_$Platform.msix" $payloadDir\CascadiaPackage.zip -Copy-Item "C:\program files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.VCLibs.Desktop\14.0\Appx\Retail\x64\Microsoft.VCLibs.x64.14.00.Desktop.appx" $payloadDir\VCLibs.zip - -# Rename it to extension of ZIP because Expand-Archive is real sassy on the build machines -# and refuses to unzip it because of its file extension while on a desktop, it just -# does the job without complaining. - -# Extract the APPX package -Expand-Archive -LiteralPath $payloadDir\CascadiaPackage.zip -DestinationPath $payloadDir\appx -Expand-Archive -LiteralPath $payloadDir\VCLibs.zip -DestinationPath $payloadDir\appx -Force +# Extract the unpackaged distribution of Windows Terminal to the payload directory, +# where it will create a subdirectory named terminal-0.0.1.0 +# This is referenced in TerminalApp.cs later as part of the test harness. +& tar -x -v -f "$repoDirectory\Artifacts\$ArtifactName\unpackaged\WindowsTerminalDev_0.0.1.0_x64.zip" -C "$payloadDir" diff --git a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs index 44e104a1a00..291c608e087 100644 --- a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs +++ b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs @@ -50,8 +50,8 @@ public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe") this.context = context; // If running locally, set WTPath to where we can find a loose deployment of Windows Terminal - // On the build machines, the scripts lay it out at the appx\ subfolder of the test deployment directory - string path = Path.GetFullPath(Path.Combine(context.TestDeploymentDir, @"appx\WindowsTerminal.exe")); + // On the build machines, the scripts lay it out at the terminal-0.0.1.0\ subfolder of the test deployment directory + string path = Path.GetFullPath(Path.Combine(context.TestDeploymentDir, @"terminal-0.0.1.0\WindowsTerminal.exe")); if (context.Properties.Contains("WTPath")) { path = (string)context.Properties["WTPath"]; From de09671d8a7f038f637eacad42cc229b2d80921d Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 6 Apr 2023 15:03:12 -0500 Subject: [PATCH 024/226] wpf: Bump the history length to 9001 instead of 1000 (#15129) This was an oversight in the original implementation. --- src/cascadia/PublicTerminalCore/HwndTerminal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index 7139f3949b8..ad4e753c9a6 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -222,7 +222,7 @@ HRESULT HwndTerminal::Initialize() _renderEngine = std::move(dxEngine); - _terminal->Create({ 80, 25 }, 1000, *_renderer); + _terminal->Create({ 80, 25 }, 9001, *_renderer); _terminal->SetWriteInputCallback([=](std::wstring_view input) noexcept { _WriteTextToConnection(input); }); localPointerToThread->EnablePainting(); From 7fbd3be8c394a1d3334a5d5a00509bf805cc7a12 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 6 Apr 2023 16:32:40 -0500 Subject: [PATCH 025/226] Only try to hand off to ConhostV1 if building for Windows (#15131) Some of our automated tooling detects this as being a private API that we're accessing via LoadLibrary/GetProcAddress. It's not *wrong*, but it's also not *right*. It's easier for us to just not do it (and save all the code for it!) in OpenConsole. --- src/features.xml | 9 ++++++++ src/host/exe/exemain.cpp | 44 ++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/features.xml b/src/features.xml index 8e77e547c4d..c11056081b3 100644 --- a/src/features.xml +++ b/src/features.xml @@ -67,6 +67,15 @@ + + Feature_LegacyConhost + conhost should support ForceV2=false, and try to load conhostv1.dll + AlwaysDisabled + + WindowsInbox + + + Feature_AtlasEngine If enabled, AtlasEngine is used by default diff --git a/src/host/exe/exemain.cpp b/src/host/exe/exemain.cpp index 8f0dce299ee..7aaa45271d9 100644 --- a/src/host/exe/exemain.cpp +++ b/src/host/exe/exemain.cpp @@ -52,6 +52,27 @@ class DefaultOutOfProcModuleWithRegistrationFlag : public OutOfProcModuleWithReg // Holds the wwinmain open until COM tells us there are no more server connections wil::unique_event _comServerExitEvent; +[[nodiscard]] static HRESULT ValidateServerHandle(const HANDLE handle) +{ + // Make sure this is a console file. + FILE_FS_DEVICE_INFORMATION DeviceInformation; + IO_STATUS_BLOCK IoStatusBlock; + const auto Status = NtQueryVolumeInformationFile(handle, &IoStatusBlock, &DeviceInformation, sizeof(DeviceInformation), FileFsDeviceInformation); + if (FAILED_NTSTATUS(Status)) + { + RETURN_NTSTATUS(Status); + } + else if (DeviceInformation.DeviceType != FILE_DEVICE_CONSOLE) + { + return E_INVALIDARG; + } + else + { + return S_OK; + } +} + +#if TIL_FEATURE_LEGACYCONHOST_ENABLED static bool useV2 = true; static bool ConhostV2ForcedInRegistry() { @@ -101,26 +122,6 @@ static bool ConhostV2ForcedInRegistry() return fShouldUseConhostV2; } -[[nodiscard]] static HRESULT ValidateServerHandle(const HANDLE handle) -{ - // Make sure this is a console file. - FILE_FS_DEVICE_INFORMATION DeviceInformation; - IO_STATUS_BLOCK IoStatusBlock; - const auto Status = NtQueryVolumeInformationFile(handle, &IoStatusBlock, &DeviceInformation, sizeof(DeviceInformation), FileFsDeviceInformation); - if (FAILED_NTSTATUS(Status)) - { - RETURN_NTSTATUS(Status); - } - else if (DeviceInformation.DeviceType != FILE_DEVICE_CONSOLE) - { - return E_INVALIDARG; - } - else - { - return S_OK; - } -} - static bool ShouldUseLegacyConhost(const ConsoleArguments& args) { if (args.InConptyMode()) @@ -185,6 +186,7 @@ static bool ShouldUseLegacyConhost(const ConsoleArguments& args) return hr; } +#endif // TIL_FEATURE_LEGACYCONHOST_ENABLED // Routine Description: // - Called back when COM says there is nothing left for our server to do and we can tear down. @@ -303,6 +305,7 @@ int CALLBACK wWinMain( else #endif { +#if TIL_FEATURE_LEGACYCONHOST_ENABLED if (ShouldUseLegacyConhost(args)) { useV2 = false; @@ -321,6 +324,7 @@ int CALLBACK wWinMain( } } if (useV2) +#endif // TIL_FEATURE_LEGACYCONHOST_ENABLED { if (args.ShouldCreateServerHandle()) { From ea44375f6dcdfb3c8bdf05b2f02e38a72d2dde09 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 6 Apr 2023 18:31:19 -0500 Subject: [PATCH 026/226] Check for updates automatically (but don't install) (#13437) This PR adds support to the About Dialog for checking the store to see if there's a new version of the Terminal package available. We'll only do this once per day, per terminal window. In dev mode, we'll always fake it and say there's an update to `x.y.z` available. This also involved pulling all of the About dialog code out into its own class. All that is goodness. We don't currently provide a button for _installing_ the update. We just check. Incremental progress is better than none. Co-authored-by: Mike Griese Co-authored-by: Leonard Hecker --- .github/actions/spelling/expect/expect.txt | 2 + src/cascadia/TerminalApp/AboutDialog.cpp | 128 ++++++++++++++++++ src/cascadia/TerminalApp/AboutDialog.h | 40 ++++++ src/cascadia/TerminalApp/AboutDialog.idl | 19 +++ src/cascadia/TerminalApp/AboutDialog.xaml | 63 +++++++++ .../Resources/en-US/Resources.resw | 12 ++ .../TerminalApp/TerminalAppLib.vcxproj | 12 ++ src/cascadia/TerminalApp/TerminalPage.cpp | 17 +-- src/cascadia/TerminalApp/TerminalPage.h | 2 - src/cascadia/TerminalApp/TerminalPage.xaml | 26 +--- src/cascadia/TerminalApp/pch.h | 1 + 11 files changed, 280 insertions(+), 42 deletions(-) create mode 100644 src/cascadia/TerminalApp/AboutDialog.cpp create mode 100644 src/cascadia/TerminalApp/AboutDialog.h create mode 100644 src/cascadia/TerminalApp/AboutDialog.idl create mode 100644 src/cascadia/TerminalApp/AboutDialog.xaml diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 92b2cadbe02..1dfaa076cd5 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -2283,6 +2283,8 @@ xunit xutr XVIRTUALSCREEN XWalk +xwwyzz +xxyyzz yact YCast YCENTER diff --git a/src/cascadia/TerminalApp/AboutDialog.cpp b/src/cascadia/TerminalApp/AboutDialog.cpp new file mode 100644 index 00000000000..4d837b65b70 --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.cpp @@ -0,0 +1,128 @@ + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "AboutDialog.h" +#include "AboutDialog.g.cpp" + +#include +#include + +#include "../../types/inc/utils.hpp" +#include "Utils.h" + +using namespace winrt; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal; +using namespace ::TerminalApp; +using namespace std::chrono_literals; + +namespace winrt +{ + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +namespace winrt::TerminalApp::implementation +{ + AboutDialog::AboutDialog() + { + InitializeComponent(); + } + + winrt::hstring AboutDialog::ApplicationDisplayName() + { + return CascadiaSettings::ApplicationDisplayName(); + } + + winrt::hstring AboutDialog::ApplicationVersion() + { + return CascadiaSettings::ApplicationVersion(); + } + + void AboutDialog::_SendFeedbackOnClick(const IInspectable& /*sender*/, const Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& /*eventArgs*/) + { +#if defined(WT_BRANDING_RELEASE) + ShellExecute(nullptr, nullptr, L"https://go.microsoft.com/fwlink/?linkid=2125419", nullptr, nullptr, SW_SHOW); +#else + ShellExecute(nullptr, nullptr, L"https://go.microsoft.com/fwlink/?linkid=2204904", nullptr, nullptr, SW_SHOW); +#endif + } + + void AboutDialog::_ThirdPartyNoticesOnClick(const IInspectable& /*sender*/, const Windows::UI::Xaml::RoutedEventArgs& /*eventArgs*/) + { + std::filesystem::path currentPath{ wil::GetModuleFileNameW(nullptr) }; + currentPath.replace_filename(L"NOTICE.html"); + ShellExecute(nullptr, nullptr, currentPath.c_str(), nullptr, nullptr, SW_SHOW); + } + + bool AboutDialog::UpdatesAvailable() const + { + return !_pendingUpdateVersion.empty(); + } + + winrt::hstring AboutDialog::PendingUpdateVersion() const + { + return _pendingUpdateVersion; + } + + void AboutDialog::_SetPendingUpdateVersion(const winrt::hstring& version) + { + _pendingUpdateVersion = version; + _PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"PendingUpdateVersion" }); + _PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"UpdatesAvailable" }); + } + + winrt::fire_and_forget AboutDialog::QueueUpdateCheck() + { + auto strongThis = get_strong(); + auto now{ std::chrono::system_clock::now() }; + if (now - _lastUpdateCheck < std::chrono::days{ 1 }) + { + co_return; + } + _lastUpdateCheck = now; + + if (!IsPackaged()) + { + co_return; + } + + co_await wil::resume_foreground(strongThis->Dispatcher()); + _SetPendingUpdateVersion({}); + CheckingForUpdates(true); + + try + { +#ifdef WT_BRANDING_DEV + // **DEV BRANDING**: Always sleep for three seconds and then report that + // there is an update available. This lets us test the system. + co_await winrt::resume_after(std::chrono::seconds{ 3 }); + co_await wil::resume_foreground(strongThis->Dispatcher()); + _SetPendingUpdateVersion(L"X.Y.Z"); +#else // release build, likely has a store context + if (auto storeContext{ winrt::Windows::Services::Store::StoreContext::GetDefault() }) + { + const auto updates = co_await storeContext.GetAppAndOptionalStorePackageUpdatesAsync(); + co_await wil::resume_foreground(strongThis->Dispatcher()); + const auto numUpdates = updates.Size(); + if (numUpdates > 0) + { + const auto update = updates.GetAt(0); + const auto version = update.Package().Id().Version(); + const auto str = fmt::format(FMT_COMPILE(L"{}.{}.{}"), version.Major, version.Minor, version.Build); + _SetPendingUpdateVersion(winrt::hstring{ str }); + } + } +#endif + } + catch (...) + { + // do nothing on failure + } + + co_await wil::resume_foreground(strongThis->Dispatcher()); + CheckingForUpdates(false); + } +} diff --git a/src/cascadia/TerminalApp/AboutDialog.h b/src/cascadia/TerminalApp/AboutDialog.h new file mode 100644 index 00000000000..a54a758f40e --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "AboutDialog.g.h" + +namespace winrt::TerminalApp::implementation +{ + struct AboutDialog : AboutDialogT + { + public: + AboutDialog(); + + winrt::hstring ApplicationDisplayName(); + winrt::hstring ApplicationVersion(); + bool UpdatesAvailable() const; + winrt::hstring PendingUpdateVersion() const; + winrt::fire_and_forget QueueUpdateCheck(); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + WINRT_OBSERVABLE_PROPERTY(bool, CheckingForUpdates, _PropertyChangedHandlers, false); + + private: + friend struct AboutDialogT; // for Xaml to bind events + + void _SetPendingUpdateVersion(const winrt::hstring& pendingUpdateVersion); + + std::chrono::system_clock::time_point _lastUpdateCheck{}; + winrt::hstring _pendingUpdateVersion; + + void _ThirdPartyNoticesOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _SendFeedbackOnClick(const IInspectable& sender, const Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& eventArgs); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(AboutDialog); +} diff --git a/src/cascadia/TerminalApp/AboutDialog.idl b/src/cascadia/TerminalApp/AboutDialog.idl new file mode 100644 index 00000000000..b00866a12a1 --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.idl @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace TerminalApp +{ + [default_interface] runtimeclass AboutDialog : Windows.UI.Xaml.Controls.ContentDialog, Windows.UI.Xaml.Data.INotifyPropertyChanged + { + AboutDialog(); + + String ApplicationDisplayName { get; }; + String ApplicationVersion { get; }; + + Boolean CheckingForUpdates { get; }; + Boolean UpdatesAvailable { get; }; + String PendingUpdateVersion { get; }; + + void QueueUpdateCheck(); + } +} diff --git a/src/cascadia/TerminalApp/AboutDialog.xaml b/src/cascadia/TerminalApp/AboutDialog.xaml new file mode 100644 index 00000000000..901bb5bfd87 --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 2d34da2f610..02b8d8d76e1 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -758,6 +758,18 @@ Suggestions found: {0} {0} will be replaced with a number. + + Checking for updates... + + + + An update is available. + + + + Install now + + The "newTabMenu" field contains more than one entry of type "remainingProfiles". Only the first one will be considered. {Locked="newTabMenu"} {Locked="remainingProfiles"} diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index b0815429e30..a22d4f6c538 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -36,6 +36,9 @@ + + Designer + Designer @@ -132,6 +135,9 @@ AppKeyBindings.idl + + AboutDialog.xaml + App.xaml @@ -231,6 +237,9 @@ ShortcutActionDispatch.idl + + AboutDialog.xaml + App.xaml @@ -256,6 +265,9 @@ + + AboutDialog.xaml + App.xaml diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index d2545d0b3ab..92fc375f8e5 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -685,6 +685,7 @@ namespace winrt::TerminalApp::implementation // Notes link, send feedback link and privacy policy link. void TerminalPage::_ShowAboutDialog() { + AboutDialog().QueueUpdateCheck(); _ShowDialogHelper(L"AboutDialog"); } @@ -698,22 +699,6 @@ namespace winrt::TerminalApp::implementation return CascadiaSettings::ApplicationVersion(); } - void TerminalPage::_SendFeedbackOnClick(const IInspectable& /*sender*/, const Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& /*eventArgs*/) - { -#if defined(WT_BRANDING_RELEASE) - ShellExecute(nullptr, nullptr, L"https://go.microsoft.com/fwlink/?linkid=2125419", nullptr, nullptr, SW_SHOW); -#else - ShellExecute(nullptr, nullptr, L"https://go.microsoft.com/fwlink/?linkid=2204904", nullptr, nullptr, SW_SHOW); -#endif - } - - void TerminalPage::_ThirdPartyNoticesOnClick(const IInspectable& /*sender*/, const Windows::UI::Xaml::RoutedEventArgs& /*eventArgs*/) - { - std::filesystem::path currentPath{ wil::GetModuleFileNameW(nullptr) }; - currentPath.replace_filename(L"NOTICE.html"); - ShellExecute(nullptr, nullptr, currentPath.c_str(), nullptr, nullptr, SW_SHOW); - } - // Method Description: // - Helper to show a content dialog // - We only open a content dialog if there isn't one open already diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 423ffea1b36..83b80bb633c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -301,8 +301,6 @@ namespace winrt::TerminalApp::implementation void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); void _CommandPaletteButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); void _AboutButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - void _ThirdPartyNoticesOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - void _SendFeedbackOnClick(const IInspectable& sender, const Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& eventArgs); void _KeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept; diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 38d7c0f69f7..22daa6fedb6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -96,30 +96,8 @@ just this weird hole that appeared in Row 0. --> - - - - - - - - - - - - - - + #include #include +#include #include #include #include From 90bbd2927dd26ac36fc3759c7e97b2fc9918b6c9 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 10 Apr 2023 15:17:29 -0500 Subject: [PATCH 027/226] Helix: Decode HTML entities in the test comment field (#15141 I have observed the test comment coming back from Helix with `"` and friends in it. It ends badly as you might imagine. This unescape will be a no-op if the data is already well-formed. --- build/Helix/ProcessHelixFiles.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Helix/ProcessHelixFiles.ps1 b/build/Helix/ProcessHelixFiles.ps1 index dcd3608a5d4..ae9f7582812 100644 --- a/build/Helix/ProcessHelixFiles.ps1 +++ b/build/Helix/ProcessHelixFiles.ps1 @@ -70,7 +70,7 @@ foreach ($testRun in $testRuns.value) foreach ($testResult in $testResults.value) { - $info = ConvertFrom-Json $testResult.comment + $info = ConvertFrom-Json ([System.Web.HttpUtility]::HtmlDecode($testResult.comment)) $helixJobId = $info.HelixJobId $helixWorkItemName = $info.HelixWorkItemName From b4f65030e3a95f74bae6ed7385595f05f7ceeadf Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 11 Apr 2023 17:10:11 -0500 Subject: [PATCH 028/226] Fix re-persisting the new legacy themes (#15160) Yep, I forgot to not write them back to the settings file here. Regressed in #15108 Closes #15152 --- .../CascadiaSettingsSerialization.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 9b73d35565e..a251622dc8c 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -56,6 +56,15 @@ constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" }; constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" }; constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" }; +static constexpr std::array builtinThemes{ + systemThemeName, + lightThemeName, + darkThemeName, + legacySystemThemeName, + legacyLightThemeName, + legacyDarkThemeName, +}; + static constexpr std::wstring_view jsonExtension{ L".json" }; static constexpr std::wstring_view FragmentsSubDirectory{ L"\\Fragments" }; static constexpr std::wstring_view FragmentsPath{ L"\\Microsoft\\Windows Terminal\\Fragments" }; @@ -566,8 +575,9 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source if (const auto theme = Theme::FromJson(themeJson)) { const auto& name{ theme->Name() }; + if (origin != OriginTag::InBox && - (name == systemThemeName || name == lightThemeName || name == darkThemeName || name == legacySystemThemeName || name == legacyDarkThemeName || name == legacyLightThemeName)) + (std::ranges::find(builtinThemes, name) != builtinThemes.end())) { // If the theme didn't come from the in-box themes, and its // name was one of the reserved names, then just ignore it. @@ -1274,7 +1284,8 @@ Json::Value CascadiaSettings::ToJson() const // Ignore the built in themes, when serializing the themes back out. We // don't want to re-include them in the user settings file. const auto theme{ winrt::get_self(entry.Value()) }; - if (theme->Name() == systemThemeName || theme->Name() == lightThemeName || theme->Name() == darkThemeName) + const auto& name{ theme->Name() }; + if (std::ranges::find(builtinThemes, name) != builtinThemes.end()) { continue; } From 508adbb1ec04e47d369e3213f178c33ed47bb53b Mon Sep 17 00:00:00 2001 From: James Holderness Date: Tue, 11 Apr 2023 23:59:25 +0100 Subject: [PATCH 029/226] Send a KeyUp sequence only once a key has been released (#15130) When win32-input-mode is enabled, we generate an input sequence for both key down and key up events. However, in the initial implementation the key up sequence for most keypresses would be faked - they were generated at the same time as the key down sequence, regardless of when the key was actually released. After this PR, we'll only generate the key up sequence once a key has actually been released. ## References and Relevant Issues The initial implementation of win32-input-mode was in PR #6309. The spec for win32-input-mode was in PR #5887. ## Validation Steps Performed I've manually tested this with an app that polls `ReadConsoleInput` in a loop and logs the results. With this PR applied, I can now see the key up events as a key is released, rather than when it was first pressed. When compared with conhost, though, there are some differences. When typing a shifted key, e.g. `Shift`+`A`, WT generates key down and key up events for both the `Shift` and the `A`, while conhost only generates both events for the `Shift` - the `A` won't generate a key up event unless you release the `Shift` before the `A`. That seems more like a conhost flaw though. Another case I tested was the Japanese Microsoft IME, which in conhost will generate a key down event for the Japanese character being inserted followed by a key up event for for `Return`. WT generates key up events for the ASCII characters being typed in the IME, then both a key down and key up event for the inserted Japanese character, and finally a key up event for `Return`. Both of those seem weird, but they still appear to work OK. The current version of WT actually produces the most sensible behavior for the IME - it just generates key up and key down events for the inserted character. But that's only because it's dropping most of the system generated key up events. Closes #8440 --- src/cascadia/TerminalCore/Terminal.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 40a506c3b32..99fa8453472 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -666,7 +666,7 @@ std::optional Terminal::GetHyperlinkIntervalFromViewportPos // - Character events (e.g. WM_CHAR) are generally the best way to properly receive // keyboard input on Windows though, as the OS is suited best at handling the // translation of the current keyboard layout, dead keys, etc. -// As a result of this false is returned for all key events that contain characters. +// As a result of this false is returned for all key down events that contain characters. // SendCharEvent may then be called with the data obtained from a character event. // - As a special case we'll always handle VK_TAB key events. // This must be done due to TermControl::_KeyDownHandler (one of the callers) @@ -728,15 +728,15 @@ bool Terminal::SendKeyEvent(const WORD vkey, const auto isSuppressedAltGrAlias = !_altGrAliasing && states.IsAltPressed() && states.IsCtrlPressed() && !states.IsAltGrPressed(); const auto ch = isSuppressedAltGrAlias ? UNICODE_NULL : _CharacterFromKeyEvent(vkey, sc, states); - // Delegate it to the character event handler if this key event can be - // mapped to one (see method description above). For Alt+key combinations + // Delegate it to the character event handler if this is a key down event that + // can be mapped to one (see method description above). For Alt+key combinations // we'll not receive another character event for some reason though. // -> Don't delegate the event if this is a Alt+key combination. // // As a special case we'll furthermore always handle VK_TAB // key events here instead of in Terminal::SendCharEvent. // See the method description for more information. - if (!isAltOnlyPressed && vkey != VK_TAB && ch != UNICODE_NULL) + if (keyDown && !isAltOnlyPressed && vkey != VK_TAB && ch != UNICODE_NULL) { return false; } @@ -818,15 +818,8 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro MarkOutputStart(); } - // Unfortunately, the UI doesn't give us both a character down and a - // character up event, only a character received event. So fake sending both - // to the terminal input translator. Unless it's in win32-input-mode, it'll - // ignore the keyup. const KeyEvent keyDown{ true, 1, vkey, scanCode, ch, states.Value() }; - const KeyEvent keyUp{ false, 1, vkey, scanCode, ch, states.Value() }; - const auto handledDown = _terminalInput->HandleKey(&keyDown); - const auto handledUp = _terminalInput->HandleKey(&keyUp); - return handledDown || handledUp; + return _terminalInput->HandleKey(&keyDown); } // Method Description: From 56d451ded7cf9ca64abc511740221b963c98ded3 Mon Sep 17 00:00:00 2001 From: Ian O'Neill Date: Wed, 12 Apr 2023 00:01:11 +0100 Subject: [PATCH 030/226] Support environment variables in the settings (#15082) Existing environment variables can be referenced by enclosing the name in percent characters (e.g. `%PATH%`). Resurrects #9287 by @christapley. Tests added and manually tested. Closes #2785 Closes #9233 Co-authored-by: Chris Tapley --- doc/cascadia/profiles.schema.json | 7 + .../LocalTests_SettingsModel/ProfileTests.cpp | 47 +++++ .../SerializationTests.cpp | 8 +- .../Resources/en-US/Resources.resw | 3 + src/cascadia/TerminalApp/TerminalPage.cpp | 17 +- src/cascadia/TerminalApp/TerminalWindow.cpp | 1 + .../TerminalConnection/ConptyConnection.cpp | 71 ++++---- .../TerminalConnection/ConptyConnection.h | 4 +- .../TerminalConnection/ConptyConnection.idl | 3 +- .../TerminalControl/IControlSettings.idl | 1 - .../CascadiaSettings.cpp | 26 +++ .../TerminalSettingsModel/CascadiaSettings.h | 1 + .../TerminalSettingsModel/MTSMSettings.h | 1 + src/cascadia/TerminalSettingsModel/Profile.h | 2 + .../TerminalSettingsModel/Profile.idl | 5 + .../TerminalSettings.cpp | 14 ++ .../TerminalSettingsModel/TerminalSettings.h | 3 +- .../TerminalSettings.idl | 5 +- .../TerminalWarnings.idl | 1 + src/cascadia/inc/ControlProperties.h | 1 - src/inc/til/env.h | 167 +++++++++++++----- src/inc/til/string.h | 31 ++++ src/types/Environment.cpp | 132 -------------- src/types/inc/Environment.hpp | 31 ---- src/types/lib/types.vcxproj | 2 - 25 files changed, 322 insertions(+), 262 deletions(-) delete mode 100644 src/types/Environment.cpp delete mode 100644 src/types/inc/Environment.hpp diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 2c62802115a..55a866a05b0 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -2574,6 +2574,13 @@ "default": false, "description": "When true, this profile should always open in an elevated context. If the window isn't running as an Administrator, then a new elevated window will be created." }, + "environment": { + "description": "Key-value pairs representing environment variables to set. Environment variable names are not case sensitive. You can reference existing environment variable names by enclosing them in literal percent characters (e.g. %PATH%).", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "experimental.autoMarkPrompts": { "default": false, "description": "When set to true, prompts will automatically be marked.", diff --git a/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp b/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp index fdb160fe275..a73b2bec3e4 100644 --- a/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp @@ -38,6 +38,9 @@ namespace SettingsModelLocalTests TEST_METHOD(LayerProfileProperties); TEST_METHOD(LayerProfileIcon); TEST_METHOD(LayerProfilesOnArray); + TEST_METHOD(ProfileWithEnvVars); + TEST_METHOD(ProfileWithEnvVarsSameNameDifferentCases); + TEST_METHOD(DuplicateProfileTest); TEST_METHOD(TestGenGuidsForProfiles); TEST_METHOD(TestCorrectOldDefaultShellPaths); @@ -349,6 +352,50 @@ namespace SettingsModelLocalTests VERIFY_ARE_NOT_EQUAL(settings->AllProfiles().GetAt(0).Guid(), settings->AllProfiles().GetAt(1).Guid()); } + void ProfileTests::ProfileWithEnvVars() + { + const std::string profileString{ R"({ + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "environment": { + "VAR_1": "value1", + "VAR_2": "value2", + "VAR_3": "%VAR_3%;value3" + } + })" }; + const auto profile = implementation::Profile::FromJson(VerifyParseSucceeded(profileString)); + std::vector envVarMaps{}; + envVarMaps.emplace_back(profile->EnvironmentVariables()); + for (auto& envMap : envVarMaps) + { + VERIFY_ARE_EQUAL(static_cast(3), envMap.Size()); + VERIFY_ARE_EQUAL(L"value1", envMap.Lookup(L"VAR_1")); + VERIFY_ARE_EQUAL(L"value2", envMap.Lookup(L"VAR_2")); + VERIFY_ARE_EQUAL(L"%VAR_3%;value3", envMap.Lookup(L"VAR_3")); + } + } + + void ProfileTests::ProfileWithEnvVarsSameNameDifferentCases() + { + const std::string userSettings{ R"({ + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "environment": { + "FOO": "VALUE", + "Foo": "Value" + } + } + ] + })" }; + const auto settings = winrt::make_self(userSettings); + const auto warnings = settings->Warnings(); + VERIFY_ARE_EQUAL(static_cast(2), warnings.Size()); + uint32_t index; + VERIFY_IS_TRUE(warnings.IndexOf(SettingsLoadWarnings::InvalidProfileEnvironmentVariables, index)); + } + void ProfileTests::TestCorrectOldDefaultShellPaths() { static constexpr std::string_view inboxProfiles{ R"({ diff --git a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp index d4cbd49aafe..ffc7dea07bd 100644 --- a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp @@ -165,7 +165,13 @@ namespace SettingsModelLocalTests "historySize": 9001, "closeOnExit": "graceful", - "experimental.retroTerminalEffect": false + "experimental.retroTerminalEffect": false, + "environment": + { + "KEY_1": "VALUE_1", + "KEY_2": "%KEY_1%", + "KEY_3": "%PATH%" + } })" }; static constexpr std::string_view smallProfileString{ R"( diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 02b8d8d76e1..e3c992f1d9f 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -282,6 +282,9 @@ Failed to parse "startupActions". {Locked="\"startupActions\""} + + Found multiple environment variables with the same name in different cases (lower/upper) - only one value will be used. + An optional command, with arguments, to be spawned in the new tab or pane diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 92fc375f8e5..240ee8eca8b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1216,7 +1216,8 @@ namespace winrt::TerminalApp::implementation nullptr, settings.InitialRows(), settings.InitialCols(), - winrt::guid()); + winrt::guid(), + profile.Guid()); if constexpr (Feature_VtPassthroughMode::IsEnabled()) { @@ -1228,12 +1229,9 @@ namespace winrt::TerminalApp::implementation else { - // profile is guaranteed to exist here - auto guidWString = Utils::GuidToString(profile.Guid()); - - StringMap envMap{}; - envMap.Insert(L"WT_PROFILE_ID", guidWString); - envMap.Insert(L"WSLENV", L"WT_PROFILE_ID"); + const auto environment = settings.EnvironmentVariables() != nullptr ? + settings.EnvironmentVariables().GetView() : + nullptr; // Update the path to be relative to whatever our CWD is. // @@ -1264,10 +1262,11 @@ namespace winrt::TerminalApp::implementation auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), newWorkingDirectory, settings.StartingTitle(), - envMap.GetView(), + environment, settings.InitialRows(), settings.InitialCols(), - winrt::guid()); + winrt::guid(), + profile.Guid()); valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough())); valueSet.Insert(L"reloadEnvironmentVariables", diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 07cc41274ee..66d8a53f597 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -50,6 +50,7 @@ static const std::array settingsLoadWarningsLabels{ USES_RESOURCE(L"InvalidColorSchemeInCmd"), USES_RESOURCE(L"InvalidSplitSize"), USES_RESOURCE(L"FailedToParseStartupActions"), + USES_RESOURCE(L"InvalidProfileEnvironmentVariables"), USES_RESOURCE(L"FailedToParseSubCommands"), USES_RESOURCE(L"UnknownTheme"), USES_RESOURCE(L"DuplicateRemainingProfilesEntry"), diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index fef88c33628..9e59098985d 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -5,12 +5,12 @@ #include "ConptyConnection.h" #include +#include #include #include #include "CTerminalHandoff.h" #include "LibraryResources.h" -#include "../../types/inc/Environment.hpp" #include "../../types/inc/utils.hpp" #include "ConptyConnection.g.cpp" @@ -84,31 +84,19 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation auto cmdline{ wil::ExpandEnvironmentStringsW(_commandline.c_str()) }; // mutable copy -- required for CreateProcessW - Utils::EnvironmentVariableMapW environment; + til::env environment; auto zeroEnvMap = wil::scope_exit([&]() noexcept { - // Can't zero the keys, but at least we can zero the values. - for (auto& [name, value] : environment) - { - ::SecureZeroMemory(value.data(), value.size() * sizeof(decltype(value.begin())::value_type)); - } - environment.clear(); }); // Populate the environment map with the current environment. if (_reloadEnvironmentVariables) { - til::env refreshedEnvironment; - refreshedEnvironment.regenerate(); - - for (auto& [key, value] : refreshedEnvironment.as_map()) - { - environment.try_emplace(key, std::move(value)); - } + environment.regenerate(); } else { - RETURN_IF_FAILED(Utils::UpdateEnvironmentMapW(environment)); + environment = til::env::from_current_environment(); } { @@ -119,39 +107,45 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const auto guidSubStr = std::wstring_view{ wsGuid }.substr(1); // Ensure every connection has the unique identifier in the environment. - environment.insert_or_assign(L"WT_SESSION", guidSubStr.data()); + environment.as_map().insert_or_assign(L"WT_SESSION", guidSubStr.data()); + // The profile Guid does include the enclosing '{}' + const auto profileGuid{ Utils::GuidToString(_profileGuid) }; + environment.as_map().insert_or_assign(L"WT_PROFILE_ID", profileGuid.data()); + + // WSLENV is a colon-delimited list of environment variables (+flags) that should appear inside WSL + // https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/ + std::wstring wslEnv{ L"WT_SESSION:WT_PROFILE_ID:" }; if (_environment) { - // add additional WT env vars like WT_SETTINGS, WT_DEFAULTS and WT_PROFILE_ID - for (auto item : _environment) + // Order the environment variable names so that resolution order is consistent + std::set keys{}; + for (const auto item : _environment) + { + keys.insert(item.Key().c_str()); + } + // add additional env vars + for (const auto& key : keys) { try { - auto key = item.Key(); // This will throw if the value isn't a string. If that // happens, then just skip this entry. - auto value = winrt::unbox_value(item.Value()); - - // avoid clobbering WSLENV - if (std::wstring_view{ key } == L"WSLENV") - { - auto current = environment[L"WSLENV"]; - value = current + L":" + value; - } + const auto value = winrt::unbox_value(_environment.Lookup(key)); - environment.insert_or_assign(key.c_str(), value.c_str()); + environment.set_user_environment_var(key.c_str(), value.c_str()); + // For each environment variable added to the environment, also add it to WSLENV + wslEnv += key + L":"; } CATCH_LOG(); } } - // WSLENV is a colon-delimited list of environment variables (+flags) that should appear inside WSL - // https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/ - - auto wslEnv = environment[L"WSLENV"]; - wslEnv = L"WT_SESSION:" + wslEnv; // prepend WT_SESSION to make sure it's visible inside WSL. - environment.insert_or_assign(L"WSLENV", wslEnv); + // We want to prepend new environment variables to WSLENV - that way if a variable already + // exists in WSLENV but with a flag, the flag will be respected. + // (This behaviour was empirically observed) + wslEnv += environment.as_map()[L"WSLENV"]; + environment.as_map().insert_or_assign(L"WSLENV", wslEnv); } std::vector newEnvVars; @@ -160,7 +154,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation newEnvVars.size() * sizeof(decltype(newEnvVars.begin())::value_type)); }); - RETURN_IF_FAILED(Utils::EnvironmentMapToEnvironmentStringsW(environment, newEnvVars)); + RETURN_IF_FAILED(environment.to_environment_strings_w(newEnvVars)); auto lpEnvironment = newEnvVars.empty() ? nullptr : newEnvVars.data(); @@ -244,7 +238,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const Windows::Foundation::Collections::IMapView& environment, uint32_t rows, uint32_t columns, - const winrt::guid& guid) + const winrt::guid& guid, + const winrt::guid& profileGuid) { Windows::Foundation::Collections::ValueSet vs{}; @@ -254,6 +249,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation vs.Insert(L"initialRows", Windows::Foundation::PropertyValue::CreateUInt32(rows)); vs.Insert(L"initialCols", Windows::Foundation::PropertyValue::CreateUInt32(columns)); vs.Insert(L"guid", Windows::Foundation::PropertyValue::CreateGuid(guid)); + vs.Insert(L"profileGuid", Windows::Foundation::PropertyValue::CreateGuid(profileGuid)); if (environment) { @@ -288,6 +284,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation } _reloadEnvironmentVariables = winrt::unbox_value_or(settings.TryLookup(L"reloadEnvironmentVariables").try_as(), _reloadEnvironmentVariables); + _profileGuid = winrt::unbox_value_or(settings.TryLookup(L"profileGuid").try_as(), _profileGuid); } if (_guid == guid{}) diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index 60528dc0463..8ac589d6462 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -52,7 +52,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const Windows::Foundation::Collections::IMapView& environment, uint32_t rows, uint32_t columns, - const winrt::guid& guid); + const winrt::guid& guid, + const winrt::guid& profileGuid); WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler); @@ -90,6 +91,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation std::array _buffer{}; bool _passthroughMode{}; bool _reloadEnvironmentVariables{}; + guid _profileGuid{}; struct StartupInfoFromDefTerm { diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl index 135a2fd2fbe..c9928d5c425 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.idl +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -31,6 +31,7 @@ namespace Microsoft.Terminal.TerminalConnection IMapView environment, UInt32 rows, UInt32 columns, - Guid guid); + Guid guid, + Guid profileGuid); }; } diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index 1e2f7d71f24..131b9432bd7 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -52,7 +52,6 @@ namespace Microsoft.Terminal.Control String Commandline { get; }; String StartingDirectory { get; }; - String EnvironmentVariables { get; }; TextAntialiasingMode AntialiasingMode { get; }; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 8a73dea58a2..851e667ac9a 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -15,6 +15,7 @@ #include #include #include +#include using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings; @@ -417,6 +418,7 @@ void CascadiaSettings::_validateSettings() _validateKeybindings(); _validateColorSchemesInCommands(); _validateThemeExists(); + _validateProfileEnvironmentVariables(); } // Method Description: @@ -541,6 +543,30 @@ void CascadiaSettings::_validateMediaResources() } } +// Method Description: +// - Checks if the profiles contain multiple environment variables with the same name, but different +// cases +void CascadiaSettings::_validateProfileEnvironmentVariables() +{ + for (const auto& profile : _allProfiles) + { + std::set envVarNames{}; + if (profile.EnvironmentVariables() == nullptr) + { + continue; + } + for (const auto [key, value] : profile.EnvironmentVariables()) + { + const auto iterator = envVarNames.insert(key.c_str()); + if (!iterator.second) + { + _warnings.Append(SettingsLoadWarnings::InvalidProfileEnvironmentVariables); + return; + } + } + } +} + // Method Description: // - Helper to get the GUID of a profile, given an optional index and a possible // "profile" value to override that. diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index b41a17c57ee..d4ad396e264 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -162,6 +162,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _validateSettings(); void _validateAllSchemesExist(); void _validateMediaResources(); + void _validateProfileEnvironmentVariables(); void _validateKeybindings() const; void _validateColorSchemesInCommands() const; bool _hasInvalidColorScheme(const Model::Command& command) const; diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 84a6002d0ee..544366fcea1 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -82,6 +82,7 @@ Author(s): X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic) \ X(hstring, TabTitle, "tabTitle") \ X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \ + X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \ X(bool, UseAtlasEngine, "useAtlasEngine", Feature_AtlasEngine::IsEnabled()) \ X(bool, RightClickContextMenu, "experimental.rightClickContextMenu", false) \ X(Windows::Foundation::Collections::IVector, BellSound, "bellSound", nullptr) \ diff --git a/src/cascadia/TerminalSettingsModel/Profile.h b/src/cascadia/TerminalSettingsModel/Profile.h index dbc33fc9ef5..05bd77cc720 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.h +++ b/src/cascadia/TerminalSettingsModel/Profile.h @@ -67,6 +67,8 @@ namespace TerminalAppUnitTests class JsonTests; }; +using IEnvironmentVariableMap = winrt::Windows::Foundation::Collections::IMap; + // GUID used for generating GUIDs at runtime, for profiles that did not have a // GUID specified manually. constexpr GUID RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID = { 0xf65ddb7e, 0x706b, 0x4499, { 0x8a, 0x50, 0x40, 0x31, 0x3c, 0xaf, 0x51, 0x0a } }; diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 0f6bf14921a..f6ab00a26c9 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -9,6 +9,8 @@ import "FontConfig.idl"; _BASE_INHERITABLE_SETTING(Type, Name); \ Microsoft.Terminal.Settings.Model.Profile Name##OverrideSource { get; } +#define COMMA , + namespace Microsoft.Terminal.Settings.Model { // This tag is used to identify the context in which the Profile was created @@ -81,6 +83,9 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, SnapOnInput); INHERITABLE_PROFILE_SETTING(Boolean, AltGrAliasing); INHERITABLE_PROFILE_SETTING(BellStyle, BellStyle); + + INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IMap, EnvironmentVariables); + INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine); INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector, BellSound); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index 091b33e444b..a69ba7086b4 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -306,6 +306,20 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _TabColor = static_cast(colorRef); } + const auto profileEnvVars = profile.EnvironmentVariables(); + if (profileEnvVars == nullptr) + { + _EnvironmentVariables = std::nullopt; + } + else + { + _EnvironmentVariables = winrt::single_threaded_map(); + for (const auto& [key, value] : profileEnvVars) + { + _EnvironmentVariables.value().Insert(key, value); + } + } + _Elevate = profile.Elevate(); _AutoMarkPrompts = Feature_ScrollbarMarks::IsEnabled() && profile.AutoMarkPrompts(); _ShowMarks = Feature_ScrollbarMarks::IsEnabled() && profile.ShowMarks(); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index cb43e24eb00..7df75e1ab5c 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -22,6 +22,7 @@ Author(s): using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IEnvironmentVariableMap = winrt::Windows::Foundation::Collections::IMap; // fwdecl unittest classes namespace SettingsModelLocalTests @@ -143,7 +144,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, hstring, StartingDirectory); INHERITABLE_SETTING(Model::TerminalSettings, hstring, StartingTitle); INHERITABLE_SETTING(Model::TerminalSettings, bool, SuppressApplicationTitle); - INHERITABLE_SETTING(Model::TerminalSettings, hstring, EnvironmentVariables); + INHERITABLE_SETTING(Model::TerminalSettings, IEnvironmentVariableMap, EnvironmentVariables); INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::ScrollbarState, ScrollState, Microsoft::Terminal::Control::ScrollbarState::Visible); INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAtlasEngine, false); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl index 514fcf7e363..3bc2c72ed0e 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl @@ -3,6 +3,8 @@ import "CascadiaSettings.idl"; +#define COMMA , + namespace Microsoft.Terminal.Settings.Model { runtimeclass TerminalSettingsCreateResult @@ -26,6 +28,8 @@ namespace Microsoft.Terminal.Settings.Model { TerminalSettings(); + Windows.Foundation.Collections.IMap EnvironmentVariables; + static TerminalSettings CreateForPreview(CascadiaSettings appSettings, Profile profile); static TerminalSettingsCreateResult CreateWithProfile(CascadiaSettings appSettings, Profile profile, Microsoft.Terminal.Control.IKeyBindings keybindings); static TerminalSettingsCreateResult CreateWithNewTerminalArgs(CascadiaSettings appSettings, NewTerminalArgs newTerminalArgs, Microsoft.Terminal.Control.IKeyBindings keybindings); @@ -39,7 +43,6 @@ namespace Microsoft.Terminal.Settings.Model // able to change these at runtime (e.g. when duplicating a pane). String Commandline { set; }; String StartingDirectory { set; }; - String EnvironmentVariables { set; }; Boolean Elevate; }; diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl index a82acf0d5db..aa02d18ff46 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl @@ -20,6 +20,7 @@ namespace Microsoft.Terminal.Settings.Model InvalidColorSchemeInCmd, InvalidSplitSize, FailedToParseStartupActions, + InvalidProfileEnvironmentVariables, FailedToParseSubCommands, UnknownTheme, DuplicateRemainingProfilesEntry, diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index 4d2bec9e209..cccde919a91 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -66,7 +66,6 @@ X(winrt::Microsoft::Terminal::Control::IKeyBindings, KeyBindings, nullptr) \ X(winrt::hstring, Commandline) \ X(winrt::hstring, StartingDirectory) \ - X(winrt::hstring, EnvironmentVariables) \ X(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible) \ X(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \ X(bool, ForceFullRepaintRendering, false) \ diff --git a/src/inc/til/env.h b/src/inc/til/env.h index 72e626a8da8..1ed0ada927f 100644 --- a/src/inc/til/env.h +++ b/src/inc/til/env.h @@ -5,6 +5,7 @@ #include #include +#include #pragma warning(push) #pragma warning(disable : 26429) // Symbol '...' is never tested for nullness, it can be marked as not_null (f.23). @@ -21,37 +22,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" namespace details { - // - // A case-insensitive wide-character map is used to store environment variables - // due to documented requirements: - // - // "All strings in the environment block must be sorted alphabetically by name. - // The sort is case-insensitive, Unicode order, without regard to locale. - // Because the equal sign is a separator, it must not be used in the name of - // an environment variable." - // https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables - // - // - Returns CSTR_LESS_THAN, CSTR_EQUAL or CSTR_GREATER_THAN - [[nodiscard]] inline int compare_string_ordinal(const std::wstring_view& lhs, const std::wstring_view& rhs) noexcept - { - const auto result = CompareStringOrdinal( - lhs.data(), - ::base::saturated_cast(lhs.size()), - rhs.data(), - ::base::saturated_cast(rhs.size()), - TRUE); - FAIL_FAST_LAST_ERROR_IF(!result); - return result; - } - - struct wstring_case_insensitive_compare - { - [[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept - { - return compare_string_ordinal(lhs, rhs) == CSTR_LESS_THAN; - } - }; - namespace vars { inline constexpr wil::zwstring_view system_root{ L"SystemRoot" }; @@ -250,7 +220,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" friend class ::EnvTests; #endif - std::map _envMap{}; + std::map _envMap{}; // We make copies of the environment variable names to ensure they are null terminated. void get(wil::zwstring_view variable) @@ -438,13 +408,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return expanded; } - void set_user_environment_var(std::wstring_view var, std::wstring_view value) - { - auto valueString = expand_environment_strings(value); - valueString = check_for_temp(var, valueString); - save_to_map(std::wstring{ var }, std::move(valueString)); - } - void concat_var(std::wstring var, std::wstring value) { if (const auto existing = _envMap.find(var); existing != _envMap.end()) @@ -475,8 +438,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { static constexpr std::wstring_view temp{ L"temp" }; static constexpr std::wstring_view tmp{ L"tmp" }; - if (til::details::compare_string_ordinal(var, temp) == CSTR_EQUAL || - til::details::compare_string_ordinal(var, tmp) == CSTR_EQUAL) + if (til::compare_string_ordinal(var, temp) == CSTR_EQUAL || + til::compare_string_ordinal(var, tmp) == CSTR_EQUAL) { return til::details::wil_env::GetShortPathNameW(value.data()); } @@ -491,9 +454,9 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" static constexpr std::wstring_view path{ L"Path" }; static constexpr std::wstring_view libPath{ L"LibPath" }; static constexpr std::wstring_view os2LibPath{ L"Os2LibPath" }; - return til::details::compare_string_ordinal(input, path) == CSTR_EQUAL || - til::details::compare_string_ordinal(input, libPath) == CSTR_EQUAL || - til::details::compare_string_ordinal(input, os2LibPath) == CSTR_EQUAL; + return til::compare_string_ordinal(input, path) == CSTR_EQUAL || + til::compare_string_ordinal(input, libPath) == CSTR_EQUAL || + til::compare_string_ordinal(input, os2LibPath) == CSTR_EQUAL; } static void strip_trailing_null(std::wstring& str) noexcept @@ -533,6 +496,35 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" parse(block); } + // Function Description: + // - Creates a new environment with the current process's unicode environment + // variables. + // Return Value: + // - A new environment + static til::env from_current_environment() + { + LPWCH currentEnvVars{}; + auto freeCurrentEnv = wil::scope_exit([&] { + if (currentEnvVars) + { + FreeEnvironmentStringsW(currentEnvVars); + currentEnvVars = nullptr; + } + }); + + currentEnvVars = ::GetEnvironmentStringsW(); + THROW_HR_IF_NULL(E_OUTOFMEMORY, currentEnvVars); + + return til::env{ currentEnvVars }; + } + + void set_user_environment_var(std::wstring_view var, std::wstring_view value) + { + auto valueString = expand_environment_strings(value); + valueString = check_for_temp(var, valueString); + save_to_map(std::wstring{ var }, std::move(valueString)); + } + void regenerate() { // Generally replicates the behavior of shell32!RegenerateUserEnvironment @@ -572,6 +564,93 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return result; } + void clear() noexcept + { + // Can't zero the keys, but at least we can zero the values. + for (auto& [name, value] : _envMap) + { + ::SecureZeroMemory(value.data(), value.size() * sizeof(decltype(value.begin())::value_type)); + } + + _envMap.clear(); + } + + // Function Description: + // - Creates a new environment block using the provided vector as appropriate + // (resizing if needed) based on the current environment variable map + // matching the format of GetEnvironmentStringsW. + // Arguments: + // - newEnvVars: The vector that will be used to create the new environment block. + // Return Value: + // - S_OK if we succeeded, or an appropriate HRESULT for failing + HRESULT to_environment_strings_w(std::vector& newEnvVars) + try + { + // Clear environment block before use. + constexpr auto cbChar{ sizeof(decltype(newEnvVars.begin())::value_type) }; + + if (!newEnvVars.empty()) + { + ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); + } + + // Resize environment block to fit map. + size_t cchEnv{ 2 }; // For the block's double NULL-terminator. + for (const auto& [name, value] : _envMap) + { + // Final form of "name=value\0". + cchEnv += name.size() + 1 + value.size() + 1; + } + newEnvVars.resize(cchEnv); + + // Ensure new block is wiped if we exit due to failure. + auto zeroNewEnv = wil::scope_exit([&]() noexcept { + ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); + }); + + // Transform each map entry and copy it into the new environment block. + auto pEnvVars{ newEnvVars.data() }; + auto cbRemaining{ cchEnv * cbChar }; + for (const auto& [name, value] : _envMap) + { + // Final form of "name=value\0" for every entry. + { + const auto cchSrc{ name.size() }; + const auto cbSrc{ cchSrc * cbChar }; + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, name.c_str(), cbSrc) != 0); + pEnvVars += cchSrc; + cbRemaining -= cbSrc; + } + + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"=", cbChar) != 0); + ++pEnvVars; + cbRemaining -= cbChar; + + { + const auto cchSrc{ value.size() }; + const auto cbSrc{ cchSrc * cbChar }; + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, value.c_str(), cbSrc) != 0); + pEnvVars += cchSrc; + cbRemaining -= cbSrc; + } + + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0", cbChar) != 0); + ++pEnvVars; + cbRemaining -= cbChar; + } + + // Environment block only has to be NULL-terminated, but double NULL-terminate anyway. + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0\0", cbChar * 2) != 0); + cbRemaining -= cbChar * 2; + + RETURN_HR_IF(E_UNEXPECTED, cbRemaining != 0); + + zeroNewEnv.release(); // success; don't wipe new environment block on exit + + return S_OK; + } + CATCH_RETURN(); + auto& as_map() noexcept { return _envMap; diff --git a/src/inc/til/string.h b/src/inc/til/string.h index 00b8c1e4eb3..fb9bd3e566d 100644 --- a/src/inc/til/string.h +++ b/src/inc/til/string.h @@ -342,4 +342,35 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { return prefix_split<>(str, needle); } + + // + // A case-insensitive wide-character map is used to store environment variables + // due to documented requirements: + // + // "All strings in the environment block must be sorted alphabetically by name. + // The sort is case-insensitive, Unicode order, without regard to locale. + // Because the equal sign is a separator, it must not be used in the name of + // an environment variable." + // https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables + // + // - Returns CSTR_LESS_THAN, CSTR_EQUAL or CSTR_GREATER_THAN + [[nodiscard]] inline int compare_string_ordinal(const std::wstring_view& lhs, const std::wstring_view& rhs) noexcept + { + const auto result = CompareStringOrdinal( + lhs.data(), + ::base::saturated_cast(lhs.size()), + rhs.data(), + ::base::saturated_cast(rhs.size()), + TRUE); + FAIL_FAST_LAST_ERROR_IF(!result); + return result; + } + + struct wstring_case_insensitive_compare + { + [[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept + { + return compare_string_ordinal(lhs, rhs) == CSTR_LESS_THAN; + } + }; } diff --git a/src/types/Environment.cpp b/src/types/Environment.cpp deleted file mode 100644 index 9e6a1118580..00000000000 --- a/src/types/Environment.cpp +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/Environment.hpp" - -using namespace ::Microsoft::Console::Utils; - -// We cannot use spand or not_null because we're dealing with \0\0-terminated buffers of unknown length -#pragma warning(disable : 26481 26429) - -// Function Description: -// - Updates an EnvironmentVariableMapW with the current process's unicode -// environment variables ignoring ones already set in the provided map. -// Arguments: -// - map: The map to populate with the current processes's environment variables. -// Return Value: -// - S_OK if we succeeded, or an appropriate HRESULT for failing -HRESULT Microsoft::Console::Utils::UpdateEnvironmentMapW(EnvironmentVariableMapW& map) noexcept -try -{ - LPWCH currentEnvVars{}; - auto freeCurrentEnv = wil::scope_exit([&] { - if (currentEnvVars) - { - FreeEnvironmentStringsW(currentEnvVars); - currentEnvVars = nullptr; - } - }); - - currentEnvVars = ::GetEnvironmentStringsW(); - RETURN_HR_IF_NULL(E_OUTOFMEMORY, currentEnvVars); - - // Each entry is NULL-terminated; block is guaranteed to be double-NULL terminated at a minimum. - for (const wchar_t* lastCh{ currentEnvVars }; *lastCh != '\0'; ++lastCh) - { - // Copy current entry into temporary map. - const auto cchEntry{ ::wcslen(lastCh) }; - const std::wstring_view entry{ lastCh, cchEntry }; - - // Every entry is of the form "name=value\0". - const auto pos = entry.find_first_of(L"=", 0, 1); - RETURN_HR_IF(E_UNEXPECTED, pos == std::wstring::npos); - - std::wstring name{ entry.substr(0, pos) }; // portion before '=' - std::wstring value{ entry.substr(pos + 1) }; // portion after '=' - - // Don't replace entries that already exist. - map.try_emplace(std::move(name), std::move(value)); - lastCh += cchEntry; - } - - return S_OK; -} -CATCH_RETURN(); - -// Function Description: -// - Creates a new environment block using the provided vector as appropriate -// (resizing if needed) based on the provided environment variable map -// matching the format of GetEnvironmentStringsW. -// Arguments: -// - map: The map to populate the new environment block vector with. -// - newEnvVars: The vector that will be used to create the new environment block. -// Return Value: -// - S_OK if we succeeded, or an appropriate HRESULT for failing -HRESULT Microsoft::Console::Utils::EnvironmentMapToEnvironmentStringsW(EnvironmentVariableMapW& map, std::vector& newEnvVars) noexcept -try -{ - // Clear environment block before use. - constexpr auto cbChar{ sizeof(decltype(newEnvVars.begin())::value_type) }; - - if (!newEnvVars.empty()) - { - ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); - } - - // Resize environment block to fit map. - size_t cchEnv{ 2 }; // For the block's double NULL-terminator. - for (const auto& [name, value] : map) - { - // Final form of "name=value\0". - cchEnv += name.size() + 1 + value.size() + 1; - } - newEnvVars.resize(cchEnv); - - // Ensure new block is wiped if we exit due to failure. - auto zeroNewEnv = wil::scope_exit([&]() noexcept { - ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); - }); - - // Transform each map entry and copy it into the new environment block. - auto pEnvVars{ newEnvVars.data() }; - auto cbRemaining{ cchEnv * cbChar }; - for (const auto& [name, value] : map) - { - // Final form of "name=value\0" for every entry. - { - const auto cchSrc{ name.size() }; - const auto cbSrc{ cchSrc * cbChar }; - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, name.c_str(), cbSrc) != 0); - pEnvVars += cchSrc; - cbRemaining -= cbSrc; - } - - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"=", cbChar) != 0); - ++pEnvVars; - cbRemaining -= cbChar; - - { - const auto cchSrc{ value.size() }; - const auto cbSrc{ cchSrc * cbChar }; - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, value.c_str(), cbSrc) != 0); - pEnvVars += cchSrc; - cbRemaining -= cbSrc; - } - - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0", cbChar) != 0); - ++pEnvVars; - cbRemaining -= cbChar; - } - - // Environment block only has to be NULL-terminated, but double NULL-terminate anyway. - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0\0", cbChar * 2) != 0); - cbRemaining -= cbChar * 2; - - RETURN_HR_IF(E_UNEXPECTED, cbRemaining != 0); - - zeroNewEnv.release(); // success; don't wipe new environment block on exit - - return S_OK; -} -CATCH_RETURN(); diff --git a/src/types/inc/Environment.hpp b/src/types/inc/Environment.hpp deleted file mode 100644 index 0994a177762..00000000000 --- a/src/types/inc/Environment.hpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -namespace Microsoft::Console::Utils -{ - // - // A case-insensitive wide-character map is used to store environment variables - // due to documented requirements: - // - // "All strings in the environment block must be sorted alphabetically by name. - // The sort is case-insensitive, Unicode order, without regard to locale. - // Because the equal sign is a separator, it must not be used in the name of - // an environment variable." - // https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables - // - struct WStringCaseInsensitiveCompare - { - [[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept - { - return (::_wcsicmp(lhs.c_str(), rhs.c_str()) < 0); - } - }; - - using EnvironmentVariableMapW = std::map; - - [[nodiscard]] HRESULT UpdateEnvironmentMapW(EnvironmentVariableMapW& map) noexcept; - - [[nodiscard]] HRESULT EnvironmentMapToEnvironmentStringsW(EnvironmentVariableMapW& map, - std::vector& newEnvVars) noexcept; - -}; diff --git a/src/types/lib/types.vcxproj b/src/types/lib/types.vcxproj index 9d015dff9bc..bceb8348e8c 100644 --- a/src/types/lib/types.vcxproj +++ b/src/types/lib/types.vcxproj @@ -15,7 +15,6 @@ - @@ -43,7 +42,6 @@ - From 10bdadffbdd150b66afe5a258b40efe39b396b79 Mon Sep 17 00:00:00 2001 From: James Pack Date: Wed, 12 Apr 2023 12:56:55 -0400 Subject: [PATCH 031/226] Skip generating a profile for rancher-desktop (#15166) Don't generate a profile for rancher-desktop utility WSL distro. Adds a check for rancher-desktop as well as docker. As mentioned in the discussion of this issue. This becomes much more difficult to maintain once other folks inevitably start to follow this pattern. But the easy win was up for grabs so I took it :) Closes #12757 --- src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp index 373282866a6..2b656154adf 100644 --- a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp @@ -11,6 +11,7 @@ static constexpr std::wstring_view WslHomeDirectory{ L"~" }; static constexpr std::wstring_view DockerDistributionPrefix{ L"docker-desktop" }; +static constexpr std::wstring_view RancherDistributionPrefix{ L"rancher-desktop" }; // The WSL entries are structured as such: // HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss @@ -78,9 +79,9 @@ static void namesToProfiles(const std::vector& names, std::vector< { for (const auto& distName : names) { - if (til::starts_with(distName, DockerDistributionPrefix)) + if (til::starts_with(distName, DockerDistributionPrefix) || til::starts_with(distName, RancherDistributionPrefix)) { - // Docker for Windows creates some utility distributions to handle Docker commands. + // Docker for Windows and Rancher for Windows creates some utility distributions to handle Docker commands. // Pursuant to GH#3556, because they are _not_ user-facing we want to hide them. continue; } From f671f065bf313f1450c7e27e271856e4cf76989a Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 12 Apr 2023 11:57:25 -0500 Subject: [PATCH 032/226] Register the GetWindowLayoutRequested handler only when ready (#15161) Moves our `GetWindowLayoutRequested` handler AFTER the xaml island is started. The `AppHost::_GetWindowLayoutAsync` handler requires us to be able to work on our UI thread, which requires that we have a `Dispatcher` ready for us to move to. If we set up this callback in the ctor, then it is possible for there to be a time slice where * the monarch creates the peasant for us, * we get ctor'ed (registering the callback) * then the monarch attempts to query all _peasants_ for their layout, coming back to ask us even before XAML has been created. I believe this was the source of the crash that was reported in a mail thread. It actually happened to me once while debugging another branch. Alas, this was realy hard to hit in the first place, so I'm not _totally_ certain this fixes it. Related to #14957 --- src/cascadia/WindowsTerminal/AppHost.cpp | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 633b3519e50..9866cb74e26 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -87,13 +87,6 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, _window->SetAutoHideWindow(_windowLogic.AutoHideWindow()); _window->MakeWindow(); - - _GetWindowLayoutRequestedToken = _peasant.GetWindowLayoutRequested([this](auto&&, - const Remoting::GetWindowLayoutArgs& args) { - // The peasants are running on separate threads, so they'll need to - // swap what context they are in to the ui thread to get the actual layout. - args.WindowLayoutJsonAsync(_GetWindowLayoutAsync()); - }); } AppHost::~AppHost() @@ -388,6 +381,23 @@ void AppHost::Initialize() _revokers.RequestMoveContent = _windowLogic.RequestMoveContent(winrt::auto_revoke, { this, &AppHost::_handleMoveContent }); _revokers.RequestReceiveContent = _windowLogic.RequestReceiveContent(winrt::auto_revoke, { this, &AppHost::_handleReceiveContent }); _revokers.SendContentRequested = _peasant.SendContentRequested(winrt::auto_revoke, { this, &AppHost::_handleSendContent }); + + // Add our GetWindowLayoutRequested handler AFTER the xaml island is + // started. Our _GetWindowLayoutAsync handler requires us to be able to work + // on our UI thread, which requires that we have a Dispatcher ready for us + // to move to. If we set up this callback in the ctor, then it is possible + // for there to be a time slice where + // * the monarch creates the peasant for us, + // * we get constructed (registering the callback) + // * then the monarch attempts to query all _peasants_ for their layout, + // coming back to ask us even before XAML has been created. + _GetWindowLayoutRequestedToken = _peasant.GetWindowLayoutRequested([this](auto&&, + const Remoting::GetWindowLayoutArgs& args) { + // The peasants are running on separate threads, so they'll need to + // swap what context they are in to the ui thread to get the actual layout. + args.WindowLayoutJsonAsync(_GetWindowLayoutAsync()); + }); + // BODGY // On certain builds of Windows, when Terminal is set as the default // it will accumulate an unbounded amount of queued animations while From 72d0566fa6d681291799e59a69168fc0483d8277 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 13 Apr 2023 13:38:38 -0500 Subject: [PATCH 033/226] Back off between attempts to start the tests (#15106) Looking through this test, I seriously don't understand how this doesn't work. I mean, I don't really get how it _does_ work, but at this point in the tests, we've actually established that both `Nihilist.exe` _and_ openconsole are running. From my read, there's no reason these should be failing at this point. We previously added a "retry 5 times" bit to this test, in #8534. That did work back then. So uh, just do that... again? --- src/host/ft_host/InitTests.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/host/ft_host/InitTests.cpp b/src/host/ft_host/InitTests.cpp index 7b7b3348494..50e5c0cf47b 100644 --- a/src/host/ft_host/InitTests.cpp +++ b/src/host/ft_host/InitTests.cpp @@ -237,11 +237,12 @@ MODULE_SETUP(ModuleSetup) // to the one that belongs to the CMD.exe in the new OpenConsole.exe window. VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FreeConsole()); - // Wait a moment for the driver to be ready after freeing to attach. VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(AttachConsole(dwFindPid)); - auto tries = 0; - while (tries < 5) + int tries = 0; + DWORD delay; + // This will wait for up to 32s in total (from 10ms to 163840ms) + for (delay = 10; delay < 30000u; delay *= 2) { tries++; Log::Comment(NoThrowString().Format(L"Attempt #%d to confirm we've attached", tries)); @@ -267,17 +268,20 @@ MODULE_SETUP(ModuleSetup) auto succeeded = GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore); if (!succeeded) { - auto gle = GetLastError(); + const auto gle = GetLastError(); VERIFY_ARE_EQUAL(6u, gle, L"If we fail to set up the console, GetLastError should return 6 here."); - Sleep(1000); + + // Sleep with a backoff, to give us longer to try next time. + WaitForSingleObject(GetCurrentThread(), delay); } else { + Log::Comment(NoThrowString().Format(L"Succeeded on try #%d", tries)); break; } }; - VERIFY_IS_LESS_THAN(tries, 5, L"Make sure we set up the new console in time"); + VERIFY_IS_LESS_THAN(delay, 30000u, L"Make sure we set up the new console in time"); return true; } From 789b0b065f9c2f0694a59adbea64525c6d8a33af Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 14 Apr 2023 12:13:07 -0500 Subject: [PATCH 034/226] Actually use the persisted position with `centerOnLaunch:true` (#15179) Fixes an issue when using both: ```json "centerOnLaunch": true, "firstWindowPreference": "persistedWindowLayout", ``` In this case, the Terminal would ignore the persisted location and still just center on launch. This has been really annoying while testing tear-out, as we keep re-opening all my debug windows as a stack on top of each other. --- src/cascadia/TerminalApp/TerminalWindow.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 66d8a53f597..f8a906b55a6 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -740,9 +740,19 @@ namespace winrt::TerminalApp::implementation { // If // * the position has been specified on the commandline, + // * we're re-opening from a persisted layout, // * We're opening the window as a part of tear out (and _contentBounds were set) // then don't center on launch - return !_contentBounds && _settings.GlobalSettings().CenterOnLaunch() && !_appArgs.GetPosition().has_value(); + bool hadPersistedPosition = false; + if (const auto layout = LoadPersistedLayout()) + { + hadPersistedPosition = (bool)layout.InitialPosition(); + } + + return !_contentBounds && + !hadPersistedPosition && + _settings.GlobalSettings().CenterOnLaunch() && + !_appArgs.GetPosition().has_value(); } // Method Description: From 21464fe41c9c09eac4b9e2d85225f18f1f3c2c7b Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 14 Apr 2023 13:07:05 -0500 Subject: [PATCH 035/226] Manually hide our DesktopWindowXamlSource (#15165) As discussed in #6507 Newer builds of Windows do this automatically. However, this was spotted in the wild on 1.18. It's possible the threading changes created a situation where the OS-side fix no longer applied to us. So let's just do it manually. It doesn't have any side effects. I saw this once on Win11, but couldn't repro it this morning when I tried to add this fix. I'm just gonna assume this worked, despite the fact that I can't repro it on win11 anymore. closes #6507 See also #14957 ## detailed description > `WindowsXamlManager::XamlCore::Initialize` calls `ConfigureCoreWindow`, which creates a `CoreWindow` on the thread > Problem is, we're calling that on the main thread (which doesn't have _any_ windows), and then eventually creating a `DesktopWindowXamlSource` on a second thread for the actual window > It's not that it "manages a window", it's that it "manages xaml on Windows OS". just use ICoreWindowInterop -- QI for ICoreWindowInterop and call get_WindowHandle. Also see: * [ICoreWindowInterop](https://learn.microsoft.com/en-us/windows/win32/api/corewindow/nn-corewindow-icorewindowinterop) * [WindowsXamlManager.InitializeForCurrentThread](https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.hosting.windowsxamlmanager.initializeforcurrentthread?view=winrt-22621#windows-ui-xaml-hosting-windowsxamlmanager-initializeforcurrentthread) * The source code in `onecoreuap\windows\dxaml\xcp\dxaml\lib\WindowsXamlManager_Partial.*` * os.2020!6102020 which fixed MSFT:33498969, MSFT:27807465, MSFT:21854264 --- src/cascadia/TerminalApp/App.cpp | 22 +++++++++++++++++++ src/cascadia/WindowsTerminal/IslandWindow.cpp | 5 +++++ src/cascadia/WindowsTerminal/IslandWindow.h | 4 ++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index b4019d95227..0a73c8bb41c 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "App.h" #include "App.g.cpp" +#include using namespace winrt; using namespace winrt::Windows::ApplicationModel::Activation; @@ -32,6 +33,27 @@ namespace winrt::TerminalApp::implementation if (!dispatcherQueue) { _windowsXamlManager = xaml::Hosting::WindowsXamlManager::InitializeForCurrentThread(); + + // As of Process Model v3, terminal windows are all created on their + // own threads, but we still initiate XAML for the App on the main + // thread. Thing is, just initializing XAML creates a CoreWindow for + // us. On Windows 10, that CoreWindow will show up as a visible + // window on the taskbar, unless we hide it manually. So, go get it + // and do the SW_HIDE thing on it. + if (const auto& coreWindow{ winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread() }) + { + if (const auto& interop{ coreWindow.try_as() }) + { + HWND coreHandle{ 0 }; + interop->get_WindowHandle(&coreHandle); + if (coreHandle) + { + // This prevents an empty "DesktopWindowXamlSource" from + // appearing on the taskbar + ShowWindow(coreHandle, SW_HIDE); + } + } + } } else { diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 7e4c5ff8aee..17a240f8fa5 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -307,6 +307,11 @@ void IslandWindow::Initialize() // stash the child interop handle so we can resize it when the main hwnd is resized interop->get_WindowHandle(&_interopWindowHandle); + // Immediately hide our XAML island hwnd. On earlier versions of Windows, + // this HWND could sometimes appear as an actual window in the taskbar + // without this! + ShowWindow(_interopWindowHandle, SW_HIDE); + _rootGrid = winrt::Windows::UI::Xaml::Controls::Grid(); _source.Content(_rootGrid); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 3673ce8f5b0..7a00528ae59 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -94,8 +94,8 @@ class IslandWindow : HWND _interopWindowHandle; - winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; - winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; + winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; // nulled in ctor + winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; // nulled in ctor wil::com_ptr _taskbar; std::function _pfnCreateCallback; From 19069e03be029d0a97c504aa235aa59355f3e6a1 Mon Sep 17 00:00:00 2001 From: James Pack Date: Fri, 14 Apr 2023 14:52:39 -0400 Subject: [PATCH 036/226] A more efficient copy assignment operator for Pane.LayoutSizeNode (#15169) ## Summary of the Pull Request This pull request updates the implementation of the copy assignment operator for Pane::LayoutSizeNode to a more efficient version and eliminates the need for the _AssignChildNode code block. ## References and Relevant Issues #11965 #11963 ## Detailed Description of the Pull Request / Additional comments My understanding of the discussion and intent of the two linked issues is that this is a more efficient way to implement the copy assignment operator for Pane.LayoutSizeNode and eliminates the need for the code block _AssignChildNode. Since both were relatively small changes, I combined the two in one PR. If that is not desirable, I can separate them. All existing tests continue to pass. image ## Validation Steps Performed All existing tests pass. No visible changes in behavior of the terminal. ## PR Checklist - [x] Closes #11963 - [x] Closes #11965 - [x] Tests added/passed - [ ] Documentation updated - If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx - [ ] Schema updated (if necessary) --- .../TerminalApp/Pane.LayoutSizeNode.cpp | 35 +++---------------- src/cascadia/TerminalApp/Pane.h | 3 -- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp b/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp index f3cc8647ff1..75215b1fe63 100644 --- a/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp +++ b/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp @@ -37,37 +37,10 @@ Pane::LayoutSizeNode& Pane::LayoutSizeNode::operator=(const LayoutSizeNode& othe size = other.size; isMinimumSize = other.isMinimumSize; - _AssignChildNode(firstChild, other.firstChild.get()); - _AssignChildNode(secondChild, other.secondChild.get()); - _AssignChildNode(nextFirstChild, other.nextFirstChild.get()); - _AssignChildNode(nextSecondChild, other.nextSecondChild.get()); + firstChild = other.firstChild ? std::make_unique(*other.firstChild) : nullptr; + secondChild = other.secondChild ? std::make_unique(*other.secondChild) : nullptr; + nextFirstChild = other.nextFirstChild ? std::make_unique(*other.nextFirstChild) : nullptr; + nextSecondChild = other.nextSecondChild ? std::make_unique(*other.nextSecondChild) : nullptr; return *this; } - -// Method Description: -// - Performs assignment operation on a single child node reusing -// - current one if present. -// Arguments: -// - nodeField: Reference to our field holding concerned node. -// - other: Node to take the values from. -// Return Value: -// - -void Pane::LayoutSizeNode::_AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode) -{ - if (newNode) - { - if (nodeField) - { - *nodeField = *newNode; - } - else - { - nodeField = std::make_unique(*newNode); - } - } - else - { - nodeField.release(); - } -} diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index a163ba2b4b5..cc844743860 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -394,9 +394,6 @@ class Pane : public std::enable_shared_from_this LayoutSizeNode(const LayoutSizeNode& other); LayoutSizeNode& operator=(const LayoutSizeNode& other); - - private: - void _AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode); }; friend struct winrt::TerminalApp::implementation::TerminalTab; From eb725e99936dcdf7b6eabd48fc0efd606c6a4925 Mon Sep 17 00:00:00 2001 From: "Mitch Capper (they, them)" Date: Fri, 14 Apr 2023 13:25:07 -0700 Subject: [PATCH 037/226] fix: WpfTerminalControl allow Connection set to null (#15062) Hides the cursor when null, shows it when not. Clear the screen any time the connection is changed. This prevents the WPF Control from crashing when set back to null, clears the console and hides the mouse as well. It sends 3 VT sequences as well now: 1) When the Connection is set to null the cursor is hidden (reflects what the default state is) 2) When the Connection is set to a value and it was null before we show the cursor (not a breaking change as requires it to have been null which previously would cause a crash, except for for set) 3) When the Connection is changed the terminal is reset. A breaking change officially although not sure if there are use cases where this behavior is not desired. For added safety we could make sure we are not being set to the same value we currently are. None of the ansi commands are needed, users could do it all themselves as well, the behavior largely seemed natural though. I didn't see any ansi constants anywhere so they are just hard coded with comments, but not sure if there is an established better practice. Closes #15061 --- .../WpfTerminalControl/TerminalContainer.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index 5480f53fce3..c9bed99c5cb 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -113,10 +113,23 @@ private get { this.connection.TerminalOutput -= this.Connection_TerminalOutput; } - + this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x001bc\x1b]104\x1b\\")); //reset console/clear screen - https://github.com/microsoft/terminal/pull/15062#issuecomment-1505654110 + var wasNull = this.connection == null; this.connection = value; - this.connection.TerminalOutput += this.Connection_TerminalOutput; - this.connection.Start(); + if (this.connection != null) + { + if (wasNull) + { + this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25h")); //show cursor + } + this.connection.TerminalOutput += this.Connection_TerminalOutput; + this.connection.Start(); + } + else + { + this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25l")); //hide cursor + } + } } From 1d354d0f5cfdee7588431db66ea0b1e5521d4e9c Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 14 Apr 2023 23:15:08 +0200 Subject: [PATCH 038/226] Fix a hang when writing wide glyphs into a 1 column terminal (#15171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code changes are mostly self-explanatory: Just skip glyphs that can never be inserted. I implemented it slightly incorrectly (a newline will be inserted every time you write such a wide glyph), but it's a niche issue and I think the simplicity of the fix is more important than its exact correctness. It also contains a fix for some severe log spam due to `_PrepareForDoubleByteSequence` complaining in this situation. The spam is so bad that it freezes the app for a few seconds during text buffer reflow. Closes #7416 ## Validation Steps Performed * Open an extra pane and run `TerminalStress.exe` in there * Resize to 1 column * Doesn't hang ✅ --- src/buffer/out/textBuffer.cpp | 4 ---- src/terminal/adapter/adaptDispatch.cpp | 24 ++++++++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 9e969c6ed24..b3c2e590e37 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -272,10 +272,6 @@ bool TextBuffer::_AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribut // - false otherwise (out of memory) bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute) { - // This function corrects most errors. If this is false, we had an uncorrectable one which - // older versions of conhost simply let pass by unflinching. - LOG_HR_IF(E_NOT_VALID_STATE, !(_AssertValidDoubleByteSequence(dbcsAttribute))); // Shouldn't be uncorrectable sequences unless something is very wrong. - auto fSuccess = true; // Now compensate if we don't have enough space for the upcoming double byte sequence // We only need to compensate for leading bytes diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 32c06d67bff..37e7125627d 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -139,17 +139,25 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) if (isWrapping) { - if (wrapAtEOL) + // We want to wrap, but we failed to write even a single character into the row. + // ROW::Write() returns the lineWidth and leaves stringIterator untouched. To prevent a + // deadlock, because stringIterator never advances, we need to throw that glyph away. + // + // This can happen under two circumstances: + // * The glyph is wider than the buffer and can never be inserted in + // the first place. There's no good way to detect this, so we check + // whether the begin column is the left margin, which is the column + // at which any legit insertion should work at a minimum. + // * The DECAWM Autowrap mode is disabled ("\x1b[?7l", !wrapAtEOL) and + // we tried writing a wide glyph into the last column which can't work. + if (textPositionBefore == textPositionAfter && (state.columnBegin == 0 || !wrapAtEOL)) { - cursor.DelayEOLWrap(); + textBuffer.ConsumeGrapheme(state.text); } - else if (textPositionBefore == textPositionAfter) + + if (wrapAtEOL) { - // We want to wrap, but we're not allowed to and we failed to write even a single character into the row. - // This can only mean one thing! The DECAWM Autowrap mode is disabled ("\x1b[?7l") and we tried writing a - // wide glyph into the last column. ROW::Write() returns the lineWidth and leaves stringIterator untouched. - // To prevent a deadlock, because stringIterator never advances, we need to throw that glyph away. - textBuffer.ConsumeGrapheme(state.text); + cursor.DelayEOLWrap(); } } } From 9b960bc88c75b3bd910c4929382713802e7e83c3 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 17 Apr 2023 08:27:52 -0500 Subject: [PATCH 039/226] Fix reordering tabs mysteriously shuffling the actual backing tabs (#15178) TL;DR: we stopped getting `TabView.TabItemsChanged`. This meant that the tab view would change its apparent order, but we wouldn't change the backing tab order. I'm fixing this by grabbing the index of the tab that starts the drag, and the index of the tab view item at the end of the drag, and using that to reorder our backing list. Closes #15121 Upstream https://github.com/microsoft/microsoft-ui-xaml/issues/8388 Regressed in #15078 - I'm pretty confident about this, since I've got a 1.18.931 build of the Terminal with tear-out, but not MUX 2.8. --- src/cascadia/TerminalApp/TabManagement.cpp | 53 ++++++++++------------ src/cascadia/TerminalApp/TerminalPage.cpp | 7 +-- src/cascadia/TerminalApp/TerminalPage.h | 5 +- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 457cf34c1ca..51918082420 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -890,34 +890,6 @@ namespace winrt::TerminalApp::implementation co_await _HandleCloseTabRequested(tab); } } - // Method Description: - // - Responds to changes in the TabView's item list by changing the - // tabview's visibility. - // - This method is also invoked when tabs are dragged / dropped as part of - // tab reordering and this method hands that case as well in concert with - // TabDragStarting and TabDragCompleted handlers that are set up in - // TerminalPage::Create() - // Arguments: - // - sender: the control that originated this event - // - eventArgs: the event's constituent arguments - void TerminalPage::_OnTabItemsChanged(const IInspectable& /*sender*/, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs) - { - if (_rearranging) - { - if (eventArgs.CollectionChange() == Windows::Foundation::Collections::CollectionChange::ItemRemoved) - { - _rearrangeFrom = eventArgs.Index(); - } - - if (eventArgs.CollectionChange() == Windows::Foundation::Collections::CollectionChange::ItemInserted) - { - _rearrangeTo = eventArgs.Index(); - } - } - - CommandPalette().Visibility(Visibility::Collapsed); - _UpdateTabView(); - } // Method Description: // - Additional responses to clicking on a TabView's item. Currently, just remove tab with middle click @@ -1079,16 +1051,37 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_TabDragStarted(const IInspectable& /*sender*/, - const IInspectable& /*eventArgs*/) + const winrt::MUX::Controls::TabViewTabDragStartingEventArgs& eventArgs) { _rearranging = true; _rearrangeFrom = std::nullopt; _rearrangeTo = std::nullopt; + + // Start tracking the index of the tab that is being dragged. In + // `_TabDragCompleted`, we'll use this to determine how to reorder our + // internal tabs list. + const auto& draggedTabViewItem{ eventArgs.Tab() }; + uint32_t tabIndexFromControl{}; + const auto tabItems{ _tabView.TabItems() }; + if (tabItems.IndexOf(draggedTabViewItem, tabIndexFromControl)) + { + // If IndexOf returns true, we've actually got an index + _rearrangeFrom = tabIndexFromControl; + } } void TerminalPage::_TabDragCompleted(const IInspectable& /*sender*/, - const IInspectable& /*eventArgs*/) + const winrt::MUX::Controls::TabViewTabDragCompletedEventArgs& eventArgs) { + const auto& draggedTabViewItem{ eventArgs.Tab() }; + + uint32_t tabIndexFromControl{}; + const auto tabItems{ _tabView.TabItems() }; + if (tabItems.IndexOf(draggedTabViewItem, tabIndexFromControl)) + { + _rearrangeTo = tabIndexFromControl; + } + auto& from{ _rearrangeFrom }; auto& to{ _rearrangeTo }; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 240ee8eca8b..fa2612c6c24 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -245,7 +245,6 @@ namespace winrt::TerminalApp::implementation }); _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); _tabView.TabCloseRequested({ this, &TerminalPage::_OnTabCloseRequested }); - _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged }); _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting }); _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver }); @@ -4720,6 +4719,8 @@ namespace winrt::TerminalApp::implementation co_await wil::resume_foreground(Dispatcher()); if (const auto& page{ weakThis.get() }) { + // `this` is safe to use + // // First we need to get the position in the List to drop to auto index = -1; @@ -4739,8 +4740,8 @@ namespace winrt::TerminalApp::implementation } } - // `this` is safe to use - const auto request = winrt::make_self(src, _WindowProperties.WindowId(), index); + const auto myId{ _WindowProperties.WindowId() }; + const auto request = winrt::make_self(src, myId, index); // This will go up to the monarch, who will then dispatch the request // back down to the source TerminalPage, who will then perform a diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 83b80bb633c..a775902396e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -412,12 +412,11 @@ namespace winrt::TerminalApp::implementation fire_and_forget _LaunchSettings(const Microsoft::Terminal::Settings::Model::SettingsTarget target); - void _TabDragStarted(const IInspectable& sender, const IInspectable& eventArgs); - void _TabDragCompleted(const IInspectable& sender, const IInspectable& eventArgs); + void _TabDragStarted(const IInspectable& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& eventArgs); + void _TabDragCompleted(const IInspectable& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragCompletedEventArgs& eventArgs); void _OnTabClick(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs); void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs); - void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs); void _OnTabCloseRequested(const IInspectable& sender, const Microsoft::UI::Xaml::Controls::TabViewTabCloseRequestedEventArgs& eventArgs); void _OnFirstLayout(const IInspectable& sender, const IInspectable& eventArgs); void _UpdatedSelectedTab(const winrt::TerminalApp::TabBase& tab); From 52171d2daba85a618440353aee09f938ac079c86 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 17 Apr 2023 08:28:29 -0500 Subject: [PATCH 040/226] Update to MUX 2.8.3 (#15183) This fixes the BreadcrumbBar issue that would crash into the debugger anytime you open the SUI on a second thread. See #14957. Maybe also tracked in #15144 - let's have @j4james test when this merges. --- dep/nuget/packages.config | 2 +- src/common.nugetversions.props | 2 +- src/common.nugetversions.targets | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index c39e95a3c85..2e4323f6912 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -7,7 +7,7 @@ - + diff --git a/src/common.nugetversions.props b/src/common.nugetversions.props index a3ac0f8ec9b..0955a1bdc45 100644 --- a/src/common.nugetversions.props +++ b/src/common.nugetversions.props @@ -37,6 +37,6 @@ - + diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index 7cffff5ec83..6c501971ab6 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -52,7 +52,7 @@ - + - + From e106c095a5ed363c53cf3eb2dc4a3f1ebcac4bdd Mon Sep 17 00:00:00 2001 From: James Pack Date: Mon, 17 Apr 2023 10:12:45 -0400 Subject: [PATCH 041/226] Enable ctrl+shift to run terminal elevated from context menu (#15137) This pull request adds the requirement for the shift key to be pressed in addition to the control key. References #14810 Implemented in #14873 This is follow up work from my last pull request that was merged that only required the control key to be pressed to launch the terminal as admin from the shell context menu. After some discussion it was decided that the shift key should be required as well as that is the norm on Windows. ## Validation Steps Performed Tested all combinations of shift+ctrl and verified that the terminal only requests elevation when a shift and control key are pressed together. The shell launches regularly if not. --- .../ShellExtension/OpenTerminalHere.cpp | 18 +++++++++--------- src/cascadia/ShellExtension/OpenTerminalHere.h | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index 34360d9d7e1..9cc84dba012 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -27,7 +27,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, IBindCtx* /*pBindContext*/) try { - const auto runElevated = IsControlPressed(); + const auto runElevated = IsControlAndShiftPressed(); wil::com_ptr_nothrow psi; RETURN_IF_FAILED(GetBestLocationFromSelectionOrSite(psiItemArray, psi.put())); @@ -204,14 +204,14 @@ HRESULT OpenTerminalHere::GetBestLocationFromSelectionOrSite(IShellItemArray* ps return S_OK; } -// This method checks if any of the ctrl keys are pressed during activation of the shell extension -bool OpenTerminalHere::IsControlPressed() +// Check is both ctrl and shift keys are pressed during activation of the shell extension +bool OpenTerminalHere::IsControlAndShiftPressed() { - const auto ControlPressed = 1U; + short control = 0; + short shift = 0; + control = GetAsyncKeyState(VK_CONTROL); + shift = GetAsyncKeyState(VK_SHIFT); - const auto control = GetKeyState(VK_CONTROL); - const auto leftControl = GetKeyState(VK_LCONTROL); - const auto rightControl = GetKeyState(VK_RCONTROL); - - return WI_IsFlagSet(control, ControlPressed) || WI_IsFlagSet(leftControl, ControlPressed) || WI_IsFlagSet(rightControl, ControlPressed); + // GetAsyncKeyState returns a value with the most significant bit set to 1 if the key is pressed. This is the sign bit. + return control < 0 && shift < 0; } diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.h b/src/cascadia/ShellExtension/OpenTerminalHere.h index f5b294942c8..f626d8112c5 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.h +++ b/src/cascadia/ShellExtension/OpenTerminalHere.h @@ -58,7 +58,7 @@ struct private: HRESULT GetLocationFromSite(IShellItem** location) const noexcept; HRESULT GetBestLocationFromSelectionOrSite(IShellItemArray* psiArray, IShellItem** location) const noexcept; - bool IsControlPressed(); + bool IsControlAndShiftPressed(); wil::com_ptr_nothrow site_; }; From 1825ca104e877f9462d8eedc3ce13be8e44a5ed5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 17 Apr 2023 09:53:59 -0500 Subject: [PATCH 042/226] fix not updating the nav view when add/removing profiles (#15162) * make the list of MenuItems observable, so the nav view can actually listen for changes to it * Use the MenuItemsSource to find the index to add at, rather than the MenuItems (which isn't accurate anymore) * Stash a single observable vector as the menuitemsource, and modify that whenever we need to do modifications. * I attempted to create a new vector, then copy into the new one, then replace the MenuItemsSource with the new vector, but that _refused_ to work. So let's just... not. Regressed in #14630 Closes #15140 Manually validated that this and #13673 are still fixed --- .../TerminalSettingsEditor/MainPage.cpp | 121 +++++++----------- .../TerminalSettingsEditor/MainPage.h | 3 + 2 files changed, 50 insertions(+), 74 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index b1b2a834933..2c18ce11c95 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -117,54 +117,18 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation lastBreadcrumb = _breadcrumbs.GetAt(size - 1); } - // Collect all the values out of the old nav view item source - auto menuItems{ SettingsNav().MenuItems() }; - - // We'll remove a bunch of items and iterate over it twice. - // --> Copy it into an STL vector to simplify our code and reduce COM overhead. - std::vector menuItemsSTL(menuItems.Size(), nullptr); - menuItems.GetMany(0, menuItemsSTL); - - // We want to refresh the list of profiles in the NavigationView. - // In order to add profiles we can use _InitializeProfilesList(); - // But before we can do that we have to remove existing profiles first of course. - // This "erase-remove" idiom will achieve just that. - menuItemsSTL.erase( - std::remove_if( - menuItemsSTL.begin(), - menuItemsSTL.end(), - [](const auto& item) -> bool { - if (const auto& navViewItem{ item.try_as() }) - { - if (const auto& tag{ navViewItem.Tag() }) - { - if (tag.try_as()) - { - // remove NavViewItem pointing to a Profile - return true; - } - if (const auto& stringTag{ tag.try_as() }) - { - if (stringTag == addProfileTag) - { - // remove the "Add Profile" item - return true; - } - } - } - } - return false; - }), - menuItemsSTL.end()); - - // Now, we've got a list of just the static entries again. Lets take - // those and stick them back into a new winrt vector, and set that as - // the source again. + // Collect only the first items out of the menu item source, the static + // ones that we don't want to regenerate. // - // By setting MenuItemsSource in its entirety, rather than manipulating - // MenuItems, we avoid a crash in WinUI. - auto newSource = winrt::single_threaded_vector(std::move(menuItemsSTL)); - SettingsNav().MenuItemsSource(newSource); + // By manipulating a MenuItemsSource this way, rather than manipulating the + // MenuItems directly, we avoid a crash in WinUI. + // + // By making the vector only _originalNumItems big to start, GetMany + // will only fill that number of elements out of the current source. + std::vector menuItemsSTL(_originalNumItems, nullptr); + _menuItemSource.GetMany(0, menuItemsSTL); + // now, just stick them back in. + _menuItemSource.ReplaceAll(menuItemsSTL); // Repopulate profile-related menu items _InitializeProfilesList(); @@ -177,7 +141,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // refresh the current page using the breadcrumb data we collected before the refresh if (const auto& crumb{ lastBreadcrumb.try_as() }) { - for (const auto& item : menuItems) + for (const auto& item : _menuItemSource) { if (const auto& menuItem{ item.try_as() }) { @@ -217,7 +181,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // Couldn't find the selected item, fallback to first menu item // This happens when the selected item was a profile which doesn't exist in the new configuration // We can use menuItemsSTL here because the only things they miss are profile entries. - const auto& firstItem{ SettingsNav().MenuItems().GetAt(0).as() }; + const auto& firstItem{ _menuItemSource.GetAt(0).as() }; SettingsNav().SelectedItem(firstItem); _Navigate(unbox_value(firstItem.Tag()), BreadcrumbSubPage::None); } @@ -251,8 +215,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { uint32_t insertIndex; auto selectedItem{ SettingsNav().SelectedItem() }; - auto menuItems{ SettingsNav().MenuItems() }; - menuItems.IndexOf(selectedItem, insertIndex); + if (_menuItemSource) + { + _menuItemSource.IndexOf(selectedItem, insertIndex); + } if (profileGuid != winrt::guid{}) { // if we were given a non-empty guid, we want to duplicate the corresponding profile @@ -545,7 +511,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _MoveXamlParsedNavItemsIntoItemSource(); } - const auto menuItems = SettingsNav().MenuItemsSource().try_as>(); // Manually create a NavigationViewItem for each profile // and keep a reference to them in a map so that we @@ -558,7 +523,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation auto profileVM = _viewModelForProfile(profile, _settingsClone); profileVM.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); auto navItem = _CreateProfileNavViewItem(profileVM); - menuItems.Append(navItem); + _menuItemSource.Append(navItem); } } @@ -572,7 +537,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation icon.Glyph(L"\xE710"); addProfileItem.Icon(icon); - menuItems.Append(addProfileItem); + _menuItemSource.Append(addProfileItem); } // BODGY @@ -592,6 +557,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } auto menuItems{ SettingsNav().MenuItems() }; + _originalNumItems = menuItems.Size(); // Remove all the existing items, and move them to a separate vector // that we'll use as a MenuItemsSource. By doing this, we avoid a WinUI // bug (MUX#6302) where modifying the NavView.Items() directly causes a @@ -599,11 +565,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // benefit of instantiating them from the XBF, rather than at runtime. // // --> Copy it into an STL vector to simplify our code and reduce COM overhead. - std::vector menuItemsSTL(menuItems.Size(), nullptr); - menuItems.GetMany(0, menuItemsSTL); + auto original = std::vector{ _originalNumItems, nullptr }; + menuItems.GetMany(0, original); - auto newSource = winrt::single_threaded_vector(std::move(menuItemsSTL)); - SettingsNav().MenuItemsSource(newSource); + _menuItemSource = winrt::single_threaded_observable_vector(std::move(original)); + + SettingsNav().MenuItemsSource(_menuItemSource); } void MainPage::_CreateAndNavigateToNewProfile(const uint32_t index, const Model::Profile& profile) @@ -612,7 +579,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto profileViewModel{ _viewModelForProfile(newProfile, _settingsClone) }; profileViewModel.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); const auto navItem{ _CreateProfileNavViewItem(profileViewModel) }; - SettingsNav().MenuItems().InsertAt(index, navItem); + + if (_menuItemSource) + { + _menuItemSource.InsertAt(index, navItem); + } // Select and navigate to the new profile SettingsNav().SelectedItem(navItem); @@ -666,22 +637,24 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // remove selected item uint32_t index; auto selectedItem{ SettingsNav().SelectedItem() }; - auto menuItems{ SettingsNav().MenuItems() }; - menuItems.IndexOf(selectedItem, index); - menuItems.RemoveAt(index); - - // navigate to the profile next to this one - const auto newSelectedItem{ menuItems.GetAt(index < menuItems.Size() - 1 ? index : index - 1) }; - SettingsNav().SelectedItem(newSelectedItem); - const auto newTag = newSelectedItem.as().Tag(); - if (const auto profileViewModel = newTag.try_as()) + if (_menuItemSource) { - profileViewModel->FocusDeleteButton(true); - _Navigate(*profileViewModel, BreadcrumbSubPage::None); - } - else - { - _Navigate(newTag.as(), BreadcrumbSubPage::None); + _menuItemSource.IndexOf(selectedItem, index); + _menuItemSource.RemoveAt(index); + + // navigate to the profile next to this one + const auto newSelectedItem{ _menuItemSource.GetAt(index < _menuItemSource.Size() - 1 ? index : index - 1) }; + SettingsNav().SelectedItem(newSelectedItem); + const auto newTag = newSelectedItem.as().Tag(); + if (const auto profileViewModel = newTag.try_as()) + { + profileViewModel->FocusDeleteButton(true); + _Navigate(*profileViewModel, BreadcrumbSubPage::None); + } + else + { + _Navigate(newTag.as(), BreadcrumbSubPage::None); + } } } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index bdcfa5b974d..90719eb09ad 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -50,6 +50,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation private: Windows::Foundation::Collections::IObservableVector _breadcrumbs; + Windows::Foundation::Collections::IObservableVector _menuItemSource; + size_t _originalNumItems = 0u; + Model::CascadiaSettings _settingsSource; Model::CascadiaSettings _settingsClone; From 2c16e7c07b496e2bfbd399d2534d7d3a0ebb1db5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 18 Apr 2023 11:23:11 -0500 Subject: [PATCH 043/226] Respect the startup info state initially passed to `wt` via ShellExecute (#13838) Original description, pre-process model v3: > This is just the `SHOWDEFAULT` bit from #12979. This seems to also work now, but I'm PR'ing it separately so it can be a separate revert from #13811, if it is problematic. More accurately: This PR enables terminal windows to use the `wShowCmd` from the STARTUPINFO passed to `windowsterminal.exe` to set the initial visibility of the window. We can't just use `SW_SHOWDEFAULT`, because all the windows are running in the initial process! After the first window, the subsequent ones would ignore any params passed to their originating `windowsterminal.exe` processes. To mitigate, we pass that `wShowCmd` info from the source process, to the actual running terminal process. That accounts for most of the delta here. Closes #9053 This doesn't do the same for defterm-initiated connections. This is because we don't need to! Defterm very explicitly rejects handoff for minimized console apps. This is probably for the best! I put an attempt in 66f8b25ec before I forgot that it was filtered long before the Terminal. NOT doing this for /min saves us all sorts of "what happens if `start /min cmd` tries to glom?" or "what if someone does `start /min cmd && start /max cmd` and they glom together?".
Also closes #15193, which was introduced as a part of this. --------- Co-authored-by: Dustin L. Howett --- .github/actions/spelling/expect/expect.txt | 1 + src/cascadia/Remoting/CommandlineArgs.h | 8 +++-- src/cascadia/Remoting/Monarch.h | 4 ++- src/cascadia/Remoting/Monarch.idl | 1 + src/cascadia/Remoting/Peasant.idl | 3 +- src/cascadia/Remoting/WindowManager.cpp | 2 +- .../ShellExtension/OpenTerminalHere.cpp | 4 +++ .../UnitTests_Remoting/RemotingTests.cpp | 34 +++++++++---------- src/cascadia/WindowsTerminal/AppHost.cpp | 9 ++++- src/cascadia/WindowsTerminal/AppHost.h | 2 ++ .../WindowsTerminal/WindowEmperor.cpp | 10 +++++- 11 files changed, 54 insertions(+), 24 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 1dfaa076cd5..c8cc9565e6a 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -804,6 +804,7 @@ HIBYTE hicon HIDEWINDOW hinst +Hirots HISTORYBUFS HISTORYNODUP HISTORYSIZE diff --git a/src/cascadia/Remoting/CommandlineArgs.h b/src/cascadia/Remoting/CommandlineArgs.h index 5277e1c5f2b..4d40c898a62 100644 --- a/src/cascadia/Remoting/CommandlineArgs.h +++ b/src/cascadia/Remoting/CommandlineArgs.h @@ -14,9 +14,11 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } CommandlineArgs(const winrt::array_view& args, - winrt::hstring currentDirectory) : + winrt::hstring currentDirectory, + const uint32_t showWindowCommand) : _args{ args.begin(), args.end() }, - _cwd{ currentDirectory } + _cwd{ currentDirectory }, + _ShowWindowCommand{ showWindowCommand } { } @@ -25,6 +27,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void Commandline(const winrt::array_view& value); winrt::com_array Commandline(); + WINRT_PROPERTY(uint32_t, ShowWindowCommand, SW_NORMAL); // SW_NORMAL is 1, 0 is SW_HIDE + private: winrt::com_array _args; winrt::hstring _cwd; diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index ef311fad4e1..3b5cc7d9abe 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -46,7 +46,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _Id{ windowInfo.Id() ? windowInfo.Id().Value() : 0 }, // We'll use 0 as a sentinel, since no window will ever get to have that ID _WindowName{ windowInfo.WindowName() }, _args{ command.Commandline() }, - _CurrentDirectory{ command.CurrentDirectory() } {}; + _CurrentDirectory{ command.CurrentDirectory() }, + _ShowWindowCommand{ command.ShowWindowCommand() } {}; WindowRequestedArgs(const winrt::hstring& window, const winrt::hstring& content, const Windows::Foundation::IReference& bounds) : _Id{ 0u }, @@ -63,6 +64,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation WINRT_PROPERTY(winrt::hstring, WindowName); WINRT_PROPERTY(winrt::hstring, CurrentDirectory); WINRT_PROPERTY(winrt::hstring, Content); + WINRT_PROPERTY(uint32_t, ShowWindowCommand, SW_NORMAL); WINRT_PROPERTY(Windows::Foundation::IReference, InitialBounds); private: diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index bc60fbe2c71..cc2c45f5426 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -26,6 +26,7 @@ namespace Microsoft.Terminal.Remoting String[] Commandline { get; }; String CurrentDirectory { get; }; + UInt32 ShowWindowCommand { get; }; String Content { get; }; Windows.Foundation.IReference InitialBounds { get; }; diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index 2b8780cae8d..89e2b52db2c 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -7,10 +7,11 @@ namespace Microsoft.Terminal.Remoting runtimeclass CommandlineArgs { CommandlineArgs(); - CommandlineArgs(String[] args, String cwd); + CommandlineArgs(String[] args, String cwd, UInt32 showWindowCommand); String[] Commandline { get; set; }; String CurrentDirectory(); + UInt32 ShowWindowCommand { get; }; }; runtimeclass RenameRequestArgs diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index f53928398cf..b0aa010402a 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -350,7 +350,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // If the name wasn't specified, this will be an empty string. p->WindowName(args.WindowName()); - p->ExecuteCommandline(*winrt::make_self(args.Commandline(), args.CurrentDirectory())); + p->ExecuteCommandline(*winrt::make_self(args.Commandline(), args.CurrentDirectory(), args.ShowWindowCommand())); _monarch.AddPeasant(*p); diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index 9cc84dba012..4b02f45f9a4 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -44,6 +44,10 @@ try STARTUPINFOEX siEx{ 0 }; siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); + // Explicitly create the terminal window visible. + siEx.StartupInfo.dwFlags |= STARTF_USESHOWWINDOW; + siEx.StartupInfo.wShowWindow = SW_SHOWNORMAL; + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; std::wstring cmdline; if (runElevated) diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 0599aa980af..0b8a07ddfe1 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -431,7 +431,7 @@ namespace RemotingUnitTests m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); std::vector args{}; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -468,7 +468,7 @@ namespace RemotingUnitTests }); std::vector args{ L"1", L"arg[1]" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -489,7 +489,7 @@ namespace RemotingUnitTests { std::vector args{ L"-1" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -497,7 +497,7 @@ namespace RemotingUnitTests } { std::vector args{ L"-2" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -534,7 +534,7 @@ namespace RemotingUnitTests winrt::clock().now() }; p1->ActivateWindow(activatedArgs); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -559,7 +559,7 @@ namespace RemotingUnitTests p2->ActivateWindow(activatedArgs); Log::Comment(L"Send a commandline to the current window, which should be p2"); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); @@ -572,7 +572,7 @@ namespace RemotingUnitTests p1->ActivateWindow(activatedArgs); Log::Comment(L"Send a commandline to the current window, which should be p1 again"); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); @@ -593,7 +593,7 @@ namespace RemotingUnitTests { std::vector args{ L"2" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -602,7 +602,7 @@ namespace RemotingUnitTests } { std::vector args{ L"10" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -648,7 +648,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to p2, who is still alive. We won't create a new window."); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -656,7 +656,7 @@ namespace RemotingUnitTests } { Log::Comment(L"Send a commandline to p1, who is dead. We will create a new window."); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -1359,7 +1359,7 @@ namespace RemotingUnitTests std::vector p2Args{ L"two", L"this is for p2" }; { - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -1368,7 +1368,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to \"two\", which should be p2"); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -1380,7 +1380,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to \"two\", who is now dead."); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -2392,7 +2392,7 @@ namespace RemotingUnitTests VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); std::vector commandlineArgs{ L"0", L"arg[1]" }; - Remoting::CommandlineArgs eventArgs{ { commandlineArgs }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { commandlineArgs }, { L"" }, SW_NORMAL }; Log::Comment(L"When we attempt to send a commandline to the MRU window," L" we should find peasant 1 (who's name is \"one\"), not 2" @@ -2577,7 +2577,7 @@ namespace RemotingUnitTests auto m0 = make_private(monarch0PID); { - Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" } }; + Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL }; const auto result = m0->ProposeCommandline(args); auto shouldCreateWindow = result.ShouldCreateWindow(); VERIFY_IS_TRUE(shouldCreateWindow); @@ -2585,7 +2585,7 @@ namespace RemotingUnitTests auto m1 = make_self(); { - Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" } }; + Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL }; try { diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 9866cb74e26..6074e3e238a 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -188,6 +188,8 @@ void AppHost::_HandleCommandlineArgs(const Remoting::WindowRequestedArgs& window } } + _launchShowWindowCommand = windowArgs.ShowWindowCommand(); + // This is a fix for GH#12190 and hopefully GH#12169. // // If the commandline we were provided is going to result in us only @@ -1247,7 +1249,12 @@ winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows:: // match the initial settings, and then call ShowWindow to finally make us // visible. - auto nCmdShow = SW_SHOW; + // Use the visibility that we were originally requested with as a base. We + // can't just use SW_SHOWDEFAULT, because that is set on a per-process + // basis. That means that a second window needs to have its STARTUPINFO's + // wShowCmd passed into the original process. + auto nCmdShow = _launchShowWindowCommand; + if (WI_IsFlagSet(_launchMode, LaunchMode::MaximizedMode)) { nCmdShow = SW_MAXIMIZE; diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 6fa0c5c2191..695a07fc895 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -43,6 +43,8 @@ class AppHost std::shared_ptr> _showHideWindowThrottler; + uint32_t _launchShowWindowCommand{ SW_NORMAL }; + void _preInit(); void _HandleCommandlineArgs(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 1284bd80370..9e851dcf8fe 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -73,7 +73,15 @@ bool WindowEmperor::HandleCommandlineArgs() _buildArgsFromCommandline(args); auto cwd{ wil::GetCurrentDirectoryW() }; - Remoting::CommandlineArgs eventArgs{ { args }, { cwd } }; + // Get the requested initial state of the window from our startup info. For + // something like `start /min`, this will set the wShowWindow member to + // SW_SHOWMINIMIZED. We'll need to make sure is bubbled all the way through, + // so we can open a new window with the same state. + STARTUPINFOW si; + GetStartupInfoW(&si); + const auto showWindow = si.wShowWindow; + + Remoting::CommandlineArgs eventArgs{ { args }, { cwd }, showWindow }; const auto isolatedMode{ _app.Logic().IsolatedMode() }; From 27bcf7e41c412f2f6826591a40456520fd38013c Mon Sep 17 00:00:00 2001 From: James Pack Date: Tue, 18 Apr 2023 12:23:17 -0400 Subject: [PATCH 044/226] Add subtext to why Always show tabs is not toggleable in SUI. (#15154) ## Summary of the Pull Request Add subtext that lets the user know why Always show tabs is not toggleable in SUI. Also adds some additional information to the comment for this value that points to the Globals_ShowTitlebar.Header setting. ## References and Relevant Issues #13984 ## Detailed Description of the Pull Request / Additional comments Simple updates to the resources that add some additional helpful information for the user. ## Validation Steps Performed Verified the updates show in the SUI and that they render correctly. ## PR Checklist - [ ] Closes #13984 - [ ] Tests added/passed - [ ] Documentation updated - If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx - [ ] Schema updated (if necessary) --------- Co-authored-by: Mike Griese --- .../TerminalSettingsEditor/Resources/en-US/Resources.resw | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index c66785fb200..6158370bbae 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -244,8 +244,8 @@ Header for a control to toggle if the app should always show the tabs (similar to a website browser). - When disabled, the tab bar will appear when a new tab is created. - A description for what the "always show tabs" setting does. Presented near "Globals_AlwaysShowTabs.Header". + When disabled, the tab bar will appear when a new tab is created. Cannot be disabled while "Hide the title bar" is On. + A description for what the "always show tabs" setting does. Presented near "Globals_AlwaysShowTabs.Header". This setting cannot be disabled while "Globals_ShowTitlebar.Header" is enabled. Position of newly created tabs From c2dd6143ac86762f96ef02de8fa7d96f88ec6a58 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 19 Apr 2023 19:42:24 +0200 Subject: [PATCH 045/226] Fix Peasant::ActivateWindow being called with an all 0 GUID (#15187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `WM_ACTIVATE` is sent on window creation, whereas `WM_SHOWWINDOW` is sent when the window is shown. Before we call `Peasant::ActivateWindow` in the `WM_ACTIVATE` handler, we try to get the virtual desktop GUID of our window, but since it's not shown yet during startup, there's also no GUID that can be retrieved. This results in an error log message and an all 0 GUID to be sent via `Peasant::ActivateWindow`. The GUID of the window that actually spawned on the other hand is never reported until the first time you reactivate it again, leading to a number of subtle bugs around window activity. Additionally, this commit fixes a race condition and pointer unsafety, by pulling all relevant member variables onto the coroutine's stack, before it yields itself to a background thread. ## Validation Steps Performed - Set a trace breakpoint on `_peasantNotifyActivateWindow` - GUID is non-zero ✅ --- src/cascadia/WindowsTerminal/AppHost.cpp | 90 ++++++++---------------- src/cascadia/WindowsTerminal/AppHost.h | 7 +- 2 files changed, 34 insertions(+), 63 deletions(-) diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 6074e3e238a..eaecfd731e1 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -32,11 +32,11 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, const Remoting::WindowManager& manager, const Remoting::Peasant& peasant) noexcept : + _appLogic{ logic }, + _windowLogic{ nullptr }, // don't make one, we're going to take a ref on app's _windowManager{ manager }, _peasant{ peasant }, - _appLogic{ logic }, // don't make one, we're going to take a ref on app's - _windowLogic{ nullptr }, - _window{ nullptr } + _desktopManager{ winrt::try_create_instance(__uuidof(VirtualDesktopManager)) } { _HandleCommandlineArgs(args); @@ -851,30 +851,39 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send _windowLogic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); } -winrt::fire_and_forget AppHost::_WindowActivated(bool activated) +void AppHost::_WindowActivated(bool activated) { _windowLogic.WindowActivated(activated); - if (!activated) + if (activated && _isWindowInitialized) { - co_return; + _peasantNotifyActivateWindow(); } +} + +winrt::fire_and_forget AppHost::_peasantNotifyActivateWindow() +{ + const auto desktopManager = _desktopManager; + const auto peasant = _peasant; + const auto hwnd = _window->GetHandle(); co_await winrt::resume_background(); - if (_peasant) + GUID currentDesktopGuid{}; + if (FAILED_LOG(desktopManager->GetWindowDesktopId(hwnd, ¤tDesktopGuid))) { - const auto currentDesktopGuid{ _CurrentDesktopGuid() }; - - // TODO: projects/5 - in the future, we'll want to actually get the - // desktop GUID in IslandWindow, and bubble that up here, then down to - // the Peasant. For now, we're just leaving space for it. - Remoting::WindowActivatedArgs args{ _peasant.GetID(), - (uint64_t)_window->GetHandle(), - currentDesktopGuid, - winrt::clock().now() }; - _peasant.ActivateWindow(args); + co_return; } + + // TODO: projects/5 - in the future, we'll want to actually get the + // desktop GUID in IslandWindow, and bubble that up here, then down to + // the Peasant. For now, we're just leaving space for it. + peasant.ActivateWindow({ + peasant.GetID(), + reinterpret_cast(hwnd), + currentDesktopGuid, + winrt::clock().now(), + }); } // Method Description: @@ -907,30 +916,6 @@ winrt::Windows::Foundation::IAsyncOperation AppHost::_GetWindowL co_return layoutJson; } -// Method Description: -// - Helper to initialize our instance of IVirtualDesktopManager. If we already -// got one, then this will just return true. Otherwise, we'll try and init a -// new instance of one, and store that. -// - This will return false if we weren't able to initialize one, which I'm not -// sure is actually possible. -// Arguments: -// - -// Return Value: -// - true iff _desktopManager points to a non-null instance of IVirtualDesktopManager -bool AppHost::_LazyLoadDesktopManager() -{ - if (_desktopManager == nullptr) - { - try - { - _desktopManager = winrt::create_instance(__uuidof(VirtualDesktopManager)); - } - CATCH_LOG(); - } - - return _desktopManager != nullptr; -} - void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*sender*/, const Remoting::SummonWindowBehavior& args) { @@ -938,7 +923,7 @@ void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*se if (args != nullptr && args.MoveToCurrentDesktop()) { - if (_LazyLoadDesktopManager()) + if (_desktopManager) { // First thing - make sure that we're not on the current desktop. If // we are, then don't call MoveWindowToDesktop. This is to mitigate @@ -966,23 +951,6 @@ void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*se } } -// Method Description: -// - This gets the GUID of the desktop our window is currently on. It does NOT -// get the GUID of the desktop that's currently active. -// Arguments: -// - -// Return Value: -// - the GUID of the desktop our window is currently on -GUID AppHost::_CurrentDesktopGuid() -{ - GUID currentDesktopGuid{ 0 }; - if (_LazyLoadDesktopManager()) - { - LOG_IF_FAILED(_desktopManager->GetWindowDesktopId(_window->GetHandle(), ¤tDesktopGuid)); - } - return currentDesktopGuid; -} - // Method Description: // - Called when this window wants _all_ windows to display their // identification. We'll hop to the BG thread, and raise an event (eventually @@ -1264,8 +1232,9 @@ winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows:: // UI thread. This is shockingly load bearing - without this, then // sometimes, we'll _still_ show the HWND before the XAML island actually // paints. - co_await winrt::resume_background(); co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher(), winrt::Windows::UI::Core::CoreDispatcherPriority::Low); + + _isWindowInitialized = true; ShowWindow(_window->GetHandle(), nCmdShow); // If we didn't start the window hidden (in one way or another), then try to @@ -1280,6 +1249,7 @@ winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows:: if (!noForeground) { SetForegroundWindow(_window->GetHandle()); + _peasantNotifyActivateWindow(); } } diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 695a07fc895..9ce5b49b05c 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -13,7 +13,7 @@ class AppHost winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, const winrt::Microsoft::Terminal::Remoting::WindowManager& manager, const winrt::Microsoft::Terminal::Remoting::Peasant& peasant) noexcept; - virtual ~AppHost(); + ~AppHost(); void AppTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, winrt::hstring newTitle); void LastTabClosed(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::LastTabClosedEventArgs& args); @@ -37,7 +37,7 @@ class AppHost winrt::Microsoft::Terminal::Remoting::Peasant _peasant{ nullptr }; winrt::com_ptr _desktopManager{ nullptr }; - + bool _isWindowInitialized = false; bool _useNonClientArea{ false }; winrt::Microsoft::Terminal::Settings::Model::LaunchMode _launchMode{}; @@ -70,7 +70,8 @@ class AppHost void _RaiseVisualBell(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& arg); void _WindowMouseWheeled(const til::point coord, const int32_t delta); - winrt::fire_and_forget _WindowActivated(bool activated); + void _WindowActivated(bool activated); + winrt::fire_and_forget _peasantNotifyActivateWindow(); void _WindowMoved(); void _DispatchCommandline(winrt::Windows::Foundation::IInspectable sender, From da0a6d468a76509831a73e5d079d8f32e0749dc5 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 19 Apr 2023 21:18:36 +0200 Subject: [PATCH 046/226] Lazy load CommandPalette and AboutDialog (#15203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This sets `x:Load` to `false` for the two elements. On my system, with Windows Defender disabled, this reduces CPU usage by 15ms and the visual delay during launch by 40ms. Part of #5907 ## Validation Steps Performed * Ctrl+Shift+P opens command palette ✅ * Context menu opens command palette ✅ * Context menu opens about dialog ✅ --- .../LocalTests_TerminalApp/TabTests.cpp | 6 +- src/cascadia/TerminalApp/AboutDialog.cpp | 3 +- src/cascadia/TerminalApp/AboutDialog.h | 5 +- src/cascadia/TerminalApp/AboutDialog.idl | 2 - .../TerminalApp/AppActionHandlers.cpp | 15 ++-- src/cascadia/TerminalApp/TabManagement.cpp | 14 +-- src/cascadia/TerminalApp/TerminalPage.cpp | 89 ++++++++++--------- src/cascadia/TerminalApp/TerminalPage.h | 3 +- src/cascadia/TerminalApp/TerminalPage.xaml | 6 +- 9 files changed, 76 insertions(+), 67 deletions(-) diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 44f1a9c2db8..63f9d1ed67a 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -1102,7 +1102,7 @@ namespace TerminalAppLocalTests // If you don't do this, the palette will just stay open, and the // next time we call _HandleNextTab, we'll continue traversing the // MRU list, instead of just hoping one entry. - page->CommandPalette().Visibility(Visibility::Collapsed); + page->LoadCommandPalette().Visibility(Visibility::Collapsed); }); TestOnUIThread([&page]() { @@ -1123,7 +1123,7 @@ namespace TerminalAppLocalTests // If you don't do this, the palette will just stay open, and the // next time we call _HandleNextTab, we'll continue traversing the // MRU list, instead of just hoping one entry. - page->CommandPalette().Visibility(Visibility::Collapsed); + page->LoadCommandPalette().Visibility(Visibility::Collapsed); }); TestOnUIThread([&page]() { @@ -1239,7 +1239,7 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"a", page->_mruTabs.GetAt(3).Title()); }); - const auto palette = winrt::get_self(page->CommandPalette()); + const auto palette = winrt::get_self(page->LoadCommandPalette()); VERIFY_ARE_EQUAL(winrt::TerminalApp::implementation::CommandPaletteMode::TabSwitchMode, palette->_currentMode, L"Verify we are in the tab switcher mode"); // At this point, the contents of the command palette's _mruTabs list is diff --git a/src/cascadia/TerminalApp/AboutDialog.cpp b/src/cascadia/TerminalApp/AboutDialog.cpp index 4d837b65b70..ac7541ed7c5 100644 --- a/src/cascadia/TerminalApp/AboutDialog.cpp +++ b/src/cascadia/TerminalApp/AboutDialog.cpp @@ -29,6 +29,7 @@ namespace winrt::TerminalApp::implementation AboutDialog::AboutDialog() { InitializeComponent(); + _queueUpdateCheck(); } winrt::hstring AboutDialog::ApplicationDisplayName() @@ -74,7 +75,7 @@ namespace winrt::TerminalApp::implementation _PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"UpdatesAvailable" }); } - winrt::fire_and_forget AboutDialog::QueueUpdateCheck() + winrt::fire_and_forget AboutDialog::_queueUpdateCheck() { auto strongThis = get_strong(); auto now{ std::chrono::system_clock::now() }; diff --git a/src/cascadia/TerminalApp/AboutDialog.h b/src/cascadia/TerminalApp/AboutDialog.h index a54a758f40e..9043027e737 100644 --- a/src/cascadia/TerminalApp/AboutDialog.h +++ b/src/cascadia/TerminalApp/AboutDialog.h @@ -16,7 +16,6 @@ namespace winrt::TerminalApp::implementation winrt::hstring ApplicationVersion(); bool UpdatesAvailable() const; winrt::hstring PendingUpdateVersion() const; - winrt::fire_and_forget QueueUpdateCheck(); WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); WINRT_OBSERVABLE_PROPERTY(bool, CheckingForUpdates, _PropertyChangedHandlers, false); @@ -24,13 +23,13 @@ namespace winrt::TerminalApp::implementation private: friend struct AboutDialogT; // for Xaml to bind events - void _SetPendingUpdateVersion(const winrt::hstring& pendingUpdateVersion); - std::chrono::system_clock::time_point _lastUpdateCheck{}; winrt::hstring _pendingUpdateVersion; + void _SetPendingUpdateVersion(const winrt::hstring& pendingUpdateVersion); void _ThirdPartyNoticesOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); void _SendFeedbackOnClick(const IInspectable& sender, const Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& eventArgs); + winrt::fire_and_forget _queueUpdateCheck(); }; } diff --git a/src/cascadia/TerminalApp/AboutDialog.idl b/src/cascadia/TerminalApp/AboutDialog.idl index b00866a12a1..5c2b08b11e1 100644 --- a/src/cascadia/TerminalApp/AboutDialog.idl +++ b/src/cascadia/TerminalApp/AboutDialog.idl @@ -13,7 +13,5 @@ namespace TerminalApp Boolean CheckingForUpdates { get; }; Boolean UpdatesAvailable { get; }; String PendingUpdateVersion { get; }; - - void QueueUpdateCheck(); } } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index c61ab8502a4..82a2a3777a5 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -613,10 +613,10 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - CommandPalette().EnableCommandPaletteMode(realArgs.LaunchMode()); - CommandPalette().Visibility(CommandPalette().Visibility() == Visibility::Visible ? - Visibility::Collapsed : - Visibility::Visible); + const auto p = LoadCommandPalette(); + const auto v = p.Visibility() == Visibility::Visible ? Visibility::Collapsed : Visibility::Visible; + p.EnableCommandPaletteMode(realArgs.LaunchMode()); + p.Visibility(v); args.Handled(true); } } @@ -799,9 +799,10 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleTabSearch(const IInspectable& /*sender*/, const ActionEventArgs& args) { - CommandPalette().SetTabs(_tabs, _mruTabs); - CommandPalette().EnableTabSearchMode(); - CommandPalette().Visibility(Visibility::Visible); + const auto p = LoadCommandPalette(); + p.SetTabs(_tabs, _mruTabs); + p.EnableTabSearchMode(); + p.Visibility(Visibility::Visible); args.Handled(true); } diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 51918082420..142dea7ccd9 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -600,13 +600,14 @@ namespace winrt::TerminalApp::implementation } else { - CommandPalette().SetTabs(_tabs, _mruTabs); + const auto p = LoadCommandPalette(); + p.SetTabs(_tabs, _mruTabs); // Otherwise, set up the tab switcher in the selected mode, with // the given ordering, and make it visible. - CommandPalette().EnableTabSwitcherMode(index, tabSwitchMode); - CommandPalette().Visibility(Visibility::Visible); - CommandPalette().SelectNextItem(bMoveRight); + p.EnableTabSwitcherMode(index, tabSwitchMode); + p.Visibility(Visibility::Visible); + p.SelectNextItem(bMoveRight); } } @@ -916,7 +917,7 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_UpdatedSelectedTab(const winrt::TerminalApp::TabBase& tab) { // Unfocus all the tabs. - for (auto tab : _tabs) + for (const auto& tab : _tabs) { tab.Focus(FocusState::Unfocused); } @@ -936,7 +937,8 @@ namespace winrt::TerminalApp::implementation // When the tab switcher is eventually dismissed, the focus will // get tossed back to the focused terminal control, so we don't // need to worry about focus getting lost. - if (CommandPalette().Visibility() != Visibility::Visible) + const auto p = CommandPaletteElement(); + if (!p || p.Visibility() != Visibility::Visible) { tab.Focus(FocusState::Programmatic); _UpdateMRUTab(tab); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index fa2612c6c24..67392e8950a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -113,13 +113,14 @@ namespace winrt::TerminalApp::implementation _settings = settings; - // Make sure to _UpdateCommandsForPalette before - // _RefreshUIForSettingsReload. _UpdateCommandsForPalette will make - // sure the KeyChordText of Commands is updated, which needs to - // happen before the Settings UI is reloaded and tries to re-read - // those values. - _UpdateCommandsForPalette(); - CommandPalette().SetActionMap(_settings.ActionMap()); + // Make sure to call SetCommands before _RefreshUIForSettingsReload. + // SetCommands will make sure the KeyChordText of Commands is updated, which needs + // to happen before the Settings UI is reloaded and tries to re-read those values. + if (const auto p = CommandPaletteElement()) + { + p.SetCommands(_settings.GlobalSettings().ActionMap().ExpandedCommands()); + p.SetActionMap(_settings.ActionMap()); + } if (needRefreshUI) { @@ -255,20 +256,6 @@ namespace winrt::TerminalApp::implementation _UpdateTabWidthMode(); - // When the visibility of the command palette changes to "collapsed", - // the palette has been closed. Toss focus back to the currently active - // control. - CommandPalette().RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { - if (CommandPalette().Visibility() == Visibility::Collapsed) - { - _FocusActiveControl(nullptr, nullptr); - } - }); - CommandPalette().DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); - CommandPalette().CommandLineExecutionRequested({ this, &TerminalPage::_OnCommandLineExecutionRequested }); - CommandPalette().SwitchToTabRequested({ this, &TerminalPage::_OnSwitchToTabRequested }); - CommandPalette().PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); - // Settings AllowDependentAnimations will affect whether animations are // enabled application-wide, so we don't need to check it each time we // want to create an animation. @@ -684,7 +671,6 @@ namespace winrt::TerminalApp::implementation // Notes link, send feedback link and privacy policy link. void TerminalPage::_ShowAboutDialog() { - AboutDialog().QueueUpdateCheck(); _ShowDialogHelper(L"AboutDialog"); } @@ -1333,8 +1319,9 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_CommandPaletteButtonOnClick(const IInspectable&, const RoutedEventArgs&) { - CommandPalette().EnableCommandPaletteMode(CommandPaletteLaunchMode::Action); - CommandPalette().Visibility(Visibility::Visible); + auto p = LoadCommandPalette(); + p.EnableCommandPaletteMode(CommandPaletteLaunchMode::Action); + p.Visibility(Visibility::Visible); } // Method Description: @@ -1350,7 +1337,7 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - Called when the users pressed keyBindings while CommandPalette is open. + // - Called when the users pressed keyBindings while CommandPaletteElement is open. // - As of GH#8480, this is also bound to the TabRowControl's KeyUp event. // That should only fire when focus is in the tab row, which is hard to // do. Notably, that's possible: @@ -1421,7 +1408,7 @@ namespace winrt::TerminalApp::implementation return; } - if (const auto p = CommandPalette(); p.Visibility() == Visibility::Visible && cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) + if (const auto p = CommandPaletteElement(); p.Visibility() == Visibility::Visible && cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) { p.Visibility(Visibility::Collapsed); } @@ -1745,6 +1732,40 @@ namespace winrt::TerminalApp::implementation } return nullptr; } + + CommandPalette TerminalPage::LoadCommandPalette() + { + if (const auto p = CommandPaletteElement()) + { + return p; + } + + return _loadCommandPaletteSlowPath(); + } + + CommandPalette TerminalPage::_loadCommandPaletteSlowPath() + { + const auto p = FindName(L"CommandPaletteElement").as(); + + p.SetCommands(_settings.GlobalSettings().ActionMap().ExpandedCommands()); + p.SetActionMap(_settings.ActionMap()); + + // When the visibility of the command palette changes to "collapsed", + // the palette has been closed. Toss focus back to the currently active control. + p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (CommandPaletteElement().Visibility() == Visibility::Collapsed) + { + _FocusActiveControl(nullptr, nullptr); + } + }); + p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + p.CommandLineExecutionRequested({ this, &TerminalPage::_OnCommandLineExecutionRequested }); + p.SwitchToTabRequested({ this, &TerminalPage::_OnSwitchToTabRequested }); + p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); + + return p; + } + // Method Description: // - Warn the user that they are about to close all open windows, then // signal that we want to close everything. @@ -3140,21 +3161,6 @@ namespace winrt::TerminalApp::implementation } } - // Method Description: - // - Repopulates the list of commands in the command palette with the - // current commands in the settings. Also updates the keybinding labels to - // reflect any matching keybindings. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_UpdateCommandsForPalette() - { - // Update the command palette when settings reload - const auto& expanded{ _settings.GlobalSettings().ActionMap().ExpandedCommands() }; - CommandPalette().SetCommands(expanded); - } - // Method Description: // - Sets the initial actions to process on startup. We'll make a copy of // this list, and process these actions when we're loaded. @@ -4828,5 +4834,4 @@ namespace winrt::TerminalApp::implementation // _RemoveTab will make sure to null out the _stashed.draggedTab _RemoveTab(*_stashed.draggedTab); } - } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index a775902396e..1d8e616538d 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -118,6 +118,7 @@ namespace winrt::TerminalApp::implementation winrt::hstring ApplicationDisplayName(); winrt::hstring ApplicationVersion(); + CommandPalette LoadCommandPalette(); winrt::fire_and_forget RequestQuit(); winrt::fire_and_forget CloseWindow(bool bypassDialog); @@ -274,6 +275,7 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::NewConnection_revoker _newConnectionRevoker; + __declspec(noinline) CommandPalette _loadCommandPaletteSlowPath(); winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); void _ShowAboutDialog(); @@ -312,7 +314,6 @@ namespace winrt::TerminalApp::implementation void _UpdateTabIcon(TerminalTab& tab); void _UpdateTabView(); void _UpdateTabWidthMode(); - void _UpdateCommandsForPalette(); void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance); void _DuplicateFocusedTab(); diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 22daa6fedb6..ade311fcde7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -97,7 +97,8 @@ --> + Grid.Row="2" + x:Load="False" /> - From 35b9e75574b2d13805f191be8e156d249ec0c66e Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 20 Apr 2023 14:31:44 +0200 Subject: [PATCH 047/226] Avoid animations during startup (#15204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes 3 sources for animations: * `TabView`'s `EntranceThemeTransition` causes tabs to slowly slide in from the bottom. Removing the transition requires you to override the entire list of transitions obviously, which is a global change. Nice. Am I glad I don't need to deal with the complexity of CSS. /s * `TabBase`, `SettingsTab` and `TerminalTab` were using a lot of coroutines with `resume_foreground` even though almost none of the functions are called from background tabs in the first place. This caused us to miss the initial XAML drawing pass, which resulted in animations when the tab icons would asynchronously pop into existence. It also appears as if `resume_foreground`, etc. have a very high CPU cost attached, which surprises me absolutely not at all given WinRT. The improvement is difficult to quantify because the run to run variation is very high. But it seems like this shaves about 10% off of the ~500ms startup delay on my PC depending on how you measure it. Part of #5907 ## PR Checklist * It starts when it should ✅ * It doesn't "exit" when it shouldn't ✅ (Scrolling, Settings reload, Bell `\a`, Progress `\e]9;4;2;80\e\\`) --- src/cascadia/TerminalApp/App.xaml | 19 +- src/cascadia/TerminalApp/SettingsTab.cpp | 26 +- src/cascadia/TerminalApp/SettingsTab.h | 2 +- src/cascadia/TerminalApp/TabBase.cpp | 90 ++++--- src/cascadia/TerminalApp/TabBase.h | 2 +- src/cascadia/TerminalApp/TerminalTab.cpp | 238 +++++++++++------- src/cascadia/TerminalApp/TerminalTab.h | 14 +- src/cascadia/TerminalControl/TermControl.cpp | 4 +- src/cascadia/TerminalControl/TermControl.xaml | 9 +- 9 files changed, 235 insertions(+), 169 deletions(-) diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml index c2a77c4461d..9b4c87a8b1f 100644 --- a/src/cascadia/TerminalApp/App.xaml +++ b/src/cascadia/TerminalApp/App.xaml @@ -5,10 +5,10 @@ + + diff --git a/src/cascadia/TerminalApp/SettingsTab.cpp b/src/cascadia/TerminalApp/SettingsTab.cpp index 7f060566059..107bbb45732 100644 --- a/src/cascadia/TerminalApp/SettingsTab.cpp +++ b/src/cascadia/TerminalApp/SettingsTab.cpp @@ -21,6 +21,8 @@ namespace winrt namespace WUX = Windows::UI::Xaml; } +#define ASSERT_UI_THREAD() assert(TabViewItem().Dispatcher().HasThreadAccess()) + namespace winrt::TerminalApp::implementation { SettingsTab::SettingsTab(MainPage settingsUI, @@ -36,6 +38,8 @@ namespace winrt::TerminalApp::implementation void SettingsTab::UpdateSettings(CascadiaSettings settings) { + ASSERT_UI_THREAD(); + auto settingsUI{ Content().as() }; settingsUI.UpdateSettings(settings); @@ -55,6 +59,8 @@ namespace winrt::TerminalApp::implementation // - The list of actions. std::vector SettingsTab::BuildStartupActions(const bool /*asContent*/) const { + ASSERT_UI_THREAD(); + ActionAndArgs action; action.Action(ShortcutAction::OpenSettings); OpenSettingsArgs args{ SettingsTarget::SettingsUI }; @@ -71,6 +77,8 @@ namespace winrt::TerminalApp::implementation // - void SettingsTab::Focus(WUX::FocusState focusState) { + ASSERT_UI_THREAD(); + _focusState = focusState; if (_focusState != FocusState::Unfocused) @@ -99,20 +107,14 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - winrt::fire_and_forget SettingsTab::_CreateIcon() + void SettingsTab::_CreateIcon() { - auto weakThis{ get_weak() }; + // This is the Setting icon (looks like a gear) + static constexpr std::wstring_view glyph{ L"\xE713" }; - co_await wil::resume_foreground(TabViewItem().Dispatcher()); - - if (auto tab{ weakThis.get() }) - { - auto glyph = L"\xE713"; // This is the Setting icon (looks like a gear) - - // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... - Icon(glyph); - TabViewItem().IconSource(IconPathConverter::IconSourceMUX(glyph)); - } + // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... + Icon(winrt::hstring{ glyph }); + TabViewItem().IconSource(IconPathConverter::IconSourceMUX(glyph)); } winrt::Windows::UI::Xaml::Media::Brush SettingsTab::_BackgroundBrush() diff --git a/src/cascadia/TerminalApp/SettingsTab.h b/src/cascadia/TerminalApp/SettingsTab.h index 1a92cf488a9..657c8387661 100644 --- a/src/cascadia/TerminalApp/SettingsTab.h +++ b/src/cascadia/TerminalApp/SettingsTab.h @@ -36,7 +36,7 @@ namespace winrt::TerminalApp::implementation winrt::Windows::UI::Xaml::ElementTheme _requestedTheme; void _MakeTabViewItem() override; - winrt::fire_and_forget _CreateIcon(); + void _CreateIcon(); virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() override; }; diff --git a/src/cascadia/TerminalApp/TabBase.cpp b/src/cascadia/TerminalApp/TabBase.cpp index 6761ab3dac1..08b696e175e 100644 --- a/src/cascadia/TerminalApp/TabBase.cpp +++ b/src/cascadia/TerminalApp/TabBase.cpp @@ -21,6 +21,8 @@ namespace winrt namespace WUX = Windows::UI::Xaml; } +#define ASSERT_UI_THREAD() assert(TabViewItem().Dispatcher().HasThreadAccess()) + namespace winrt::TerminalApp::implementation { WUX::FocusState TabBase::FocusState() const noexcept @@ -32,6 +34,8 @@ namespace winrt::TerminalApp::implementation // - Prepares this tab for being removed from the UI hierarchy void TabBase::Shutdown() { + ASSERT_UI_THREAD(); + Content(nullptr); _ClosedHandlers(nullptr, nullptr); } @@ -159,6 +163,8 @@ namespace winrt::TerminalApp::implementation void TabBase::UpdateTabViewIndex(const uint32_t idx, const uint32_t numTabs) { + ASSERT_UI_THREAD(); + TabViewIndex(idx); TabViewNumTabs(numTabs); _EnableCloseMenuItems(); @@ -167,11 +173,15 @@ namespace winrt::TerminalApp::implementation void TabBase::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch) { + ASSERT_UI_THREAD(); + _dispatch = dispatch; } void TabBase::SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap) { + ASSERT_UI_THREAD(); + _actionMap = actionMap; _UpdateSwitchToTabKeyChord(); } @@ -183,26 +193,18 @@ namespace winrt::TerminalApp::implementation // - keyChord - string representation of the key chord that switches to the current tab // Return Value: // - - winrt::fire_and_forget TabBase::_UpdateSwitchToTabKeyChord() + void TabBase::_UpdateSwitchToTabKeyChord() { const auto keyChord = _actionMap ? _actionMap.GetKeyBindingForAction(ShortcutAction::SwitchToTab, SwitchToTabArgs{ _TabViewIndex }) : nullptr; const auto keyChordText = keyChord ? KeyChordSerialization::ToString(keyChord) : L""; if (_keyChord == keyChordText) { - co_return; + return; } _keyChord = keyChordText; - - auto weakThis{ get_weak() }; - - co_await wil::resume_foreground(TabViewItem().Dispatcher()); - - if (auto tab{ weakThis.get() }) - { - _UpdateToolTip(); - } + _UpdateToolTip(); } // Method Description: @@ -281,6 +283,8 @@ namespace winrt::TerminalApp::implementation std::optional TabBase::GetTabColor() { + ASSERT_UI_THREAD(); + return std::nullopt; } @@ -288,6 +292,8 @@ namespace winrt::TerminalApp::implementation const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& unfocused, const til::color& tabRowColor) { + ASSERT_UI_THREAD(); + _themeColor = focused; _unfocusedThemeColor = unfocused; _tabRowColor = tabRowColor; @@ -305,49 +311,37 @@ namespace winrt::TerminalApp::implementation // - void TabBase::_RecalculateAndApplyTabColor() { - auto weakThis{ get_weak() }; - - TabViewItem().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis]() { - auto ptrTab = weakThis.get(); - if (!ptrTab) - { - return; - } - - auto tab{ ptrTab }; + // GetTabColor will return the color set by the color picker, or the + // color specified in the profile. If neither of those were set, + // then look to _themeColor to see if there's a value there. + // Otherwise, clear our color, falling back to the TabView defaults. + const auto currentColor = GetTabColor(); + if (currentColor.has_value()) + { + _ApplyTabColorOnUIThread(currentColor.value()); + } + else if (_themeColor != nullptr) + { + // Safely get the active control's brush. + const Media::Brush terminalBrush{ _BackgroundBrush() }; - // GetTabColor will return the color set by the color picker, or the - // color specified in the profile. If neither of those were set, - // then look to _themeColor to see if there's a value there. - // Otherwise, clear our color, falling back to the TabView defaults. - const auto currentColor = tab->GetTabColor(); - if (currentColor.has_value()) + if (const auto themeBrush{ _themeColor.Evaluate(Application::Current().Resources(), terminalBrush, false) }) { - tab->_ApplyTabColorOnUIThread(currentColor.value()); - } - else if (tab->_themeColor != nullptr) - { - // Safely get the active control's brush. - const Media::Brush terminalBrush{ tab->_BackgroundBrush() }; - - if (const auto themeBrush{ tab->_themeColor.Evaluate(Application::Current().Resources(), terminalBrush, false) }) - { - // ThemeColor.Evaluate will get us a Brush (because the - // TermControl could have an acrylic BG, for example). Take - // that brush, and get the color out of it. We don't really - // want to have the tab items themselves be acrylic. - tab->_ApplyTabColorOnUIThread(til::color{ ThemeColor::ColorFromBrush(themeBrush) }); - } - else - { - tab->_ClearTabBackgroundColor(); - } + // ThemeColor.Evaluate will get us a Brush (because the + // TermControl could have an acrylic BG, for example). Take + // that brush, and get the color out of it. We don't really + // want to have the tab items themselves be acrylic. + _ApplyTabColorOnUIThread(til::color{ ThemeColor::ColorFromBrush(themeBrush) }); } else { - tab->_ClearTabBackgroundColor(); + _ClearTabBackgroundColor(); } - }); + } + else + { + _ClearTabBackgroundColor(); + } } // Method Description: diff --git a/src/cascadia/TerminalApp/TabBase.h b/src/cascadia/TerminalApp/TabBase.h index ca2d0bb7b12..1832ebf16f3 100644 --- a/src/cascadia/TerminalApp/TabBase.h +++ b/src/cascadia/TerminalApp/TabBase.h @@ -69,7 +69,7 @@ namespace winrt::TerminalApp::implementation void _EnableCloseMenuItems(); void _CloseTabsAfter(); void _CloseOtherTabs(); - winrt::fire_and_forget _UpdateSwitchToTabKeyChord(); + void _UpdateSwitchToTabKeyChord(); void _UpdateToolTip(); void _RecalculateAndApplyTabColor(); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 446f74097f8..a3259199b18 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -24,6 +24,8 @@ namespace winrt namespace WUX = Windows::UI::Xaml; } +#define ASSERT_UI_THREAD() assert(TabViewItem().Dispatcher().HasThreadAccess()) + namespace winrt::TerminalApp::implementation { TerminalTab::TerminalTab(std::shared_ptr rootPane) @@ -143,30 +145,23 @@ namespace winrt::TerminalApp::implementation _RecalculateAndApplyTabColor(); } - winrt::fire_and_forget TerminalTab::_UpdateHeaderControlMaxWidth() + void TerminalTab::_UpdateHeaderControlMaxWidth() { - auto weakThis{ get_weak() }; - - co_await wil::resume_foreground(TabViewItem().Dispatcher()); - - if (auto tab{ weakThis.get() }) + try { - try + // Make sure to try/catch this, because the LocalTests won't be + // able to use this helper. + const auto settings{ winrt::TerminalApp::implementation::AppLogic::CurrentAppSettings() }; + if (settings.GlobalSettings().TabWidthMode() == winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::SizeToContent) { - // Make sure to try/catch this, because the LocalTests won't be - // able to use this helper. - const auto settings{ winrt::TerminalApp::implementation::AppLogic::CurrentAppSettings() }; - if (settings.GlobalSettings().TabWidthMode() == winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::SizeToContent) - { - tab->_headerControl.RenamerMaxWidth(HeaderRenameBoxWidthTitleLength); - } - else - { - tab->_headerControl.RenamerMaxWidth(HeaderRenameBoxWidthDefault); - } + _headerControl.RenamerMaxWidth(HeaderRenameBoxWidthTitleLength); + } + else + { + _headerControl.RenamerMaxWidth(HeaderRenameBoxWidthDefault); } - CATCH_LOG() } + CATCH_LOG() } // Method Description: @@ -182,6 +177,8 @@ namespace winrt::TerminalApp::implementation // that was last focused. TermControl TerminalTab::GetActiveTerminalControl() const { + ASSERT_UI_THREAD(); + if (_activePane) { return _activePane->GetLastFocusedTerminalControl(); @@ -198,6 +195,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::Initialize() { + ASSERT_UI_THREAD(); + _rootPane->WalkTree([&](std::shared_ptr pane) { // Attach event handlers to each new pane _AttachEventHandlersToPane(pane); @@ -217,6 +216,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::Focus(WUX::FocusState focusState) { + ASSERT_UI_THREAD(); + _focusState = focusState; if (_focusState != FocusState::Unfocused) @@ -249,6 +250,8 @@ namespace winrt::TerminalApp::implementation // focused, else the GUID of the profile of the last control to be focused Profile TerminalTab::GetFocusedProfile() const noexcept { + ASSERT_UI_THREAD(); + return _activePane->GetFocusedProfile(); } @@ -260,6 +263,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::UpdateSettings() { + ASSERT_UI_THREAD(); + // The tabWidthMode may have changed, update the header control accordingly _UpdateHeaderControlMaxWidth(); } @@ -270,12 +275,14 @@ namespace winrt::TerminalApp::implementation // - iconPath: The new path string to use as the IconPath for our TabViewItem // Return Value: // - - winrt::fire_and_forget TerminalTab::UpdateIcon(const winrt::hstring iconPath) + void TerminalTab::UpdateIcon(const winrt::hstring iconPath) { + ASSERT_UI_THREAD(); + // Don't reload our icon if it hasn't changed. if (iconPath == _lastIconPath) { - co_return; + return; } _lastIconPath = iconPath; @@ -284,19 +291,12 @@ namespace winrt::TerminalApp::implementation // for when we show the icon again) if (_iconHidden) { - co_return; + return; } - auto weakThis{ get_weak() }; - - co_await wil::resume_foreground(TabViewItem().Dispatcher()); - - if (auto tab{ weakThis.get() }) - { - // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... - Icon(_lastIconPath); - TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath)); - } + // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... + Icon(_lastIconPath); + TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath)); } // Method Description: @@ -304,28 +304,23 @@ namespace winrt::TerminalApp::implementation // - Used when we want to show the progress ring, which should replace the icon // Arguments: // - hide: if true, we hide the icon; if false, we show the icon - winrt::fire_and_forget TerminalTab::HideIcon(const bool hide) + void TerminalTab::HideIcon(const bool hide) { - auto weakThis{ get_weak() }; - - co_await wil::resume_foreground(TabViewItem().Dispatcher()); + ASSERT_UI_THREAD(); - if (auto tab{ weakThis.get() }) + if (_iconHidden != hide) { - if (tab->_iconHidden != hide) + if (hide) { - if (hide) - { - Icon({}); - TabViewItem().IconSource(IconSource{ nullptr }); - } - else - { - Icon(_lastIconPath); - TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath)); - } - tab->_iconHidden = hide; + Icon({}); + TabViewItem().IconSource(IconSource{ nullptr }); } + else + { + Icon(_lastIconPath); + TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath)); + } + _iconHidden = hide; } } @@ -333,37 +328,27 @@ namespace winrt::TerminalApp::implementation // - Hide or show the bell indicator in the tab header // Arguments: // - show: if true, we show the indicator; if false, we hide the indicator - winrt::fire_and_forget TerminalTab::ShowBellIndicator(const bool show) + void TerminalTab::ShowBellIndicator(const bool show) { - auto weakThis{ get_weak() }; + ASSERT_UI_THREAD(); - co_await wil::resume_foreground(TabViewItem().Dispatcher()); - - if (auto tab{ weakThis.get() }) - { - _tabStatus.BellIndicator(show); - } + _tabStatus.BellIndicator(show); } // Method Description: // - Activates the timer for the bell indicator in the tab // - Called if a bell raised when the tab already has focus - winrt::fire_and_forget TerminalTab::ActivateBellIndicatorTimer() + void TerminalTab::ActivateBellIndicatorTimer() { - auto weakThis{ get_weak() }; - - co_await wil::resume_foreground(TabViewItem().Dispatcher()); + ASSERT_UI_THREAD(); - if (auto tab{ weakThis.get() }) + if (!_bellIndicatorTimer.has_value()) { - if (!tab->_bellIndicatorTimer.has_value()) - { - DispatcherTimer bellIndicatorTimer; - bellIndicatorTimer.Interval(std::chrono::milliseconds(2000)); - bellIndicatorTimer.Tick({ get_weak(), &TerminalTab::_BellIndicatorTimerTick }); - bellIndicatorTimer.Start(); - tab->_bellIndicatorTimer.emplace(std::move(bellIndicatorTimer)); - } + DispatcherTimer bellIndicatorTimer; + bellIndicatorTimer.Interval(std::chrono::milliseconds(2000)); + bellIndicatorTimer.Tick({ get_weak(), &TerminalTab::_BellIndicatorTimerTick }); + bellIndicatorTimer.Start(); + _bellIndicatorTimer.emplace(std::move(bellIndicatorTimer)); } } @@ -396,21 +381,18 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - winrt::fire_and_forget TerminalTab::UpdateTitle() + void TerminalTab::UpdateTitle() { - auto weakThis{ get_weak() }; - co_await wil::resume_foreground(TabViewItem().Dispatcher()); - if (auto tab{ weakThis.get() }) - { - const auto activeTitle = _GetActiveTitle(); - // Bubble our current tab text to anyone who's listening for changes. - Title(activeTitle); - - // Update the control to reflect the changed title - _headerControl.Title(activeTitle); - Automation::AutomationProperties::SetName(tab->TabViewItem(), activeTitle); - _UpdateToolTip(); - } + ASSERT_UI_THREAD(); + + const auto activeTitle = _GetActiveTitle(); + // Bubble our current tab text to anyone who's listening for changes. + Title(activeTitle); + + // Update the control to reflect the changed title + _headerControl.Title(activeTitle); + Automation::AutomationProperties::SetName(TabViewItem(), activeTitle); + _UpdateToolTip(); } // Method Description: @@ -421,12 +403,11 @@ namespace winrt::TerminalApp::implementation // - delta: a number of lines to move the viewport relative to the current viewport. // Return Value: // - - winrt::fire_and_forget TerminalTab::Scroll(const int delta) + void TerminalTab::Scroll(const int delta) { - auto control = GetActiveTerminalControl(); - - co_await wil::resume_foreground(control.Dispatcher()); + ASSERT_UI_THREAD(); + auto control = GetActiveTerminalControl(); const auto currentOffset = control.ScrollOffset(); control.ScrollViewport(::base::ClampAdd(currentOffset, delta)); } @@ -440,6 +421,8 @@ namespace winrt::TerminalApp::implementation // - A vector of commands std::vector TerminalTab::BuildStartupActions(const bool asContent) const { + ASSERT_UI_THREAD(); + // Give initial ids (0 for the child created with this tab, // 1 for the child after the first split. auto state = _rootPane->BuildStartupActions(0, 1, asContent); @@ -513,6 +496,8 @@ namespace winrt::TerminalApp::implementation const float splitSize, std::shared_ptr pane) { + ASSERT_UI_THREAD(); + // Add the new event handlers to the new pane(s) // and update their ids. pane->WalkTree([&](auto p) { @@ -560,6 +545,8 @@ namespace winrt::TerminalApp::implementation // - The removed pane, if the remove succeeded. std::shared_ptr TerminalTab::DetachPane() { + ASSERT_UI_THREAD(); + // if we only have one pane, or the focused pane is the root, remove it // entirely and close this tab if (_rootPane == _activePane) @@ -587,6 +574,8 @@ namespace winrt::TerminalApp::implementation // - The root pane. std::shared_ptr TerminalTab::DetachRoot() { + ASSERT_UI_THREAD(); + // remove the closed event handler since we are closing the tab // manually. _rootPane->Closed(_rootClosedToken); @@ -613,6 +602,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::AttachPane(std::shared_ptr pane) { + ASSERT_UI_THREAD(); + // Add the new event handlers to the new pane(s) // and update their ids. pane->WalkTree([&](auto p) { @@ -658,6 +649,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::AttachColorPicker(TerminalApp::ColorPickupFlyout& colorPicker) { + ASSERT_UI_THREAD(); + auto weakThis{ get_weak() }; _tabColorPickup = colorPicker; @@ -696,6 +689,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::ToggleSplitOrientation() { + ASSERT_UI_THREAD(); + _rootPane->ToggleSplitOrientation(); } @@ -703,6 +698,8 @@ namespace winrt::TerminalApp::implementation // - See Pane::CalcSnappedDimension float TerminalTab::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { + ASSERT_UI_THREAD(); + return _rootPane->CalcSnappedDimension(widthOrHeight, dimension); } @@ -715,6 +712,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::ResizePane(const ResizeDirection& direction) { + ASSERT_UI_THREAD(); + // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. _rootPane->ResizePane(direction); @@ -730,6 +729,8 @@ namespace winrt::TerminalApp::implementation // to the terminal when no other panes are present (GH#6219) bool TerminalTab::NavigateFocus(const FocusDirection& direction) { + ASSERT_UI_THREAD(); + // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. if (const auto newFocus = _rootPane->NavigateDirection(_activePane, direction, _mruPanes)) @@ -760,6 +761,8 @@ namespace winrt::TerminalApp::implementation // - true if two panes were swapped. bool TerminalTab::SwapPane(const FocusDirection& direction) { + ASSERT_UI_THREAD(); + // You cannot swap panes with the parent/child pane because of the // circular reference. if (direction == FocusDirection::Parent || direction == FocusDirection::Child) @@ -783,6 +786,8 @@ namespace winrt::TerminalApp::implementation bool TerminalTab::FocusPane(const uint32_t id) { + ASSERT_UI_THREAD(); + if (_rootPane == nullptr) { return false; @@ -797,6 +802,8 @@ namespace winrt::TerminalApp::implementation // - Prepares this tab for being removed from the UI hierarchy by shutting down all active connections. void TerminalTab::Shutdown() { + ASSERT_UI_THREAD(); + if (_rootPane) { _rootPane->Shutdown(); @@ -813,22 +820,30 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::ClosePane() { + ASSERT_UI_THREAD(); + _activePane->Close(); } void TerminalTab::SetTabText(winrt::hstring title) { + ASSERT_UI_THREAD(); + _runtimeTabText = title; UpdateTitle(); } winrt::hstring TerminalTab::GetTabText() const { + ASSERT_UI_THREAD(); + return _runtimeTabText; } void TerminalTab::ResetTabText() { + ASSERT_UI_THREAD(); + _runtimeTabText = L""; UpdateTitle(); } @@ -842,6 +857,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::ActivateTabRenamer() { + ASSERT_UI_THREAD(); + _headerControl.BeginRename(); } @@ -889,7 +906,8 @@ namespace winrt::TerminalApp::implementation auto dispatcher = TabViewItem().Dispatcher(); ControlEventTokens events{}; - events.titleToken = control.TitleChanged([weakThis](auto&&, auto&&) { + events.titleToken = control.TitleChanged([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget { + co_await wil::resume_foreground(dispatcher); // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) { @@ -899,7 +917,8 @@ namespace winrt::TerminalApp::implementation } }); - events.colorToken = control.TabColorChanged([weakThis](auto&&, auto&&) { + events.colorToken = control.TabColorChanged([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget { + co_await wil::resume_foreground(dispatcher); if (auto tab{ weakThis.get() }) { // The control's tabColor changed, but it is not necessarily the @@ -918,14 +937,16 @@ namespace winrt::TerminalApp::implementation } }); - events.readOnlyToken = control.ReadOnlyChanged([weakThis](auto&&, auto&&) { + events.readOnlyToken = control.ReadOnlyChanged([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget { + co_await wil::resume_foreground(dispatcher); if (auto tab{ weakThis.get() }) { tab->_RecalculateAndApplyReadOnly(); } }); - events.focusToken = control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) { + events.focusToken = control.FocusFollowMouseRequested([dispatcher, weakThis](auto&& sender, auto&&) -> winrt::fire_and_forget { + co_await wil::resume_foreground(dispatcher); if (const auto tab{ weakThis.get() }) { if (tab->_focusState != FocusState::Unfocused) @@ -955,6 +976,8 @@ namespace winrt::TerminalApp::implementation // progress percentage of all our panes. winrt::TerminalApp::TaskbarState TerminalTab::GetCombinedTaskbarState() const { + ASSERT_UI_THREAD(); + std::vector states; if (_rootPane) { @@ -1114,13 +1137,11 @@ namespace winrt::TerminalApp::implementation // Add a Closed event handler to the Pane. If the pane closes out from // underneath us, and it's zoomed, we want to be able to make sure to // update our state accordingly to un-zoom that pane. See GH#7252. - auto closedToken = pane->Closed([weakThis, weakPane](auto&& /*s*/, auto&& /*e*/) -> winrt::fire_and_forget { + auto closedToken = pane->Closed([weakThis, weakPane](auto&& /*s*/, auto&& /*e*/) { if (auto tab{ weakThis.get() }) { if (tab->_zoomedPane) { - co_await wil::resume_foreground(tab->Content().Dispatcher()); - tab->Content(tab->_rootPane->GetRootElement()); tab->ExitZoom(); } @@ -1133,7 +1154,6 @@ namespace winrt::TerminalApp::implementation // did not actually change. Triggering if (pane != tab->_activePane && !tab->_activePane->_IsLeaf()) { - co_await wil::resume_foreground(tab->Content().Dispatcher()); tab->_UpdateActivePane(tab->_activePane); } @@ -1387,6 +1407,8 @@ namespace winrt::TerminalApp::implementation // - The tab's color, if any std::optional TerminalTab::GetTabColor() { + ASSERT_UI_THREAD(); + std::optional controlTabColor; if (const auto& control = GetActiveTerminalControl()) { @@ -1425,6 +1447,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::SetRuntimeTabColor(const winrt::Windows::UI::Color& color) { + ASSERT_UI_THREAD(); + _runtimeTabColor.emplace(color); _RecalculateAndApplyTabColor(); } @@ -1439,6 +1463,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::ResetRuntimeTabColor() { + ASSERT_UI_THREAD(); + _runtimeTabColor.reset(); _RecalculateAndApplyTabColor(); } @@ -1462,6 +1488,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::RequestColorPicker() { + ASSERT_UI_THREAD(); + _ColorPickerRequestedHandlers(); } @@ -1473,6 +1501,8 @@ namespace winrt::TerminalApp::implementation // - The total number of leaf panes hosted by this tab. int TerminalTab::GetLeafPaneCount() const noexcept { + ASSERT_UI_THREAD(); + return _rootPane->GetLeafPaneCount(); } @@ -1490,6 +1520,8 @@ namespace winrt::TerminalApp::implementation const float splitSize, winrt::Windows::Foundation::Size availableSpace) const { + ASSERT_UI_THREAD(); + return _rootPane->PreCalculateCanSplit(_activePane, splitType, splitSize, availableSpace).value_or(std::nullopt); } @@ -1501,6 +1533,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::UpdateZoom(std::shared_ptr newFocus) { + ASSERT_UI_THREAD(); + // clear the existing content so the old zoomed pane can be added back to the root tree Content(nullptr); _rootPane->Restore(_zoomedPane); @@ -1521,6 +1555,8 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::ToggleZoom() { + ASSERT_UI_THREAD(); + if (_zoomedPane) { ExitZoom(); @@ -1533,6 +1569,8 @@ namespace winrt::TerminalApp::implementation void TerminalTab::EnterZoom() { + ASSERT_UI_THREAD(); + // Clear the content first, because with parent focusing it is possible // to zoom the root pane, but setting the content will not trigger the // property changed event since it is the same and you would end up with @@ -1546,6 +1584,8 @@ namespace winrt::TerminalApp::implementation } void TerminalTab::ExitZoom() { + ASSERT_UI_THREAD(); + Content(nullptr); _rootPane->Restore(_zoomedPane); _zoomedPane = nullptr; @@ -1556,6 +1596,8 @@ namespace winrt::TerminalApp::implementation bool TerminalTab::IsZoomed() { + ASSERT_UI_THREAD(); + return _zoomedPane != nullptr; } @@ -1565,6 +1607,8 @@ namespace winrt::TerminalApp::implementation // the same read-only status. void TerminalTab::TogglePaneReadOnly() { + ASSERT_UI_THREAD(); + auto hasReadOnly = false; auto allReadOnly = true; _activePane->WalkTree([&](const auto& p) { @@ -1642,6 +1686,8 @@ namespace winrt::TerminalApp::implementation std::shared_ptr TerminalTab::GetActivePane() const { + ASSERT_UI_THREAD(); + return _activePane; } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 7809b9c7612..1986faad567 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -28,7 +28,7 @@ namespace winrt::TerminalApp::implementation void Focus(winrt::Windows::UI::Xaml::FocusState focusState) override; - winrt::fire_and_forget Scroll(const int delta); + void Scroll(const int delta); std::shared_ptr DetachRoot(); std::shared_ptr DetachPane(); @@ -41,11 +41,11 @@ namespace winrt::TerminalApp::implementation std::shared_ptr newPane); void ToggleSplitOrientation(); - winrt::fire_and_forget UpdateIcon(const winrt::hstring iconPath); - winrt::fire_and_forget HideIcon(const bool hide); + void UpdateIcon(const winrt::hstring iconPath); + void HideIcon(const bool hide); - winrt::fire_and_forget ShowBellIndicator(const bool show); - winrt::fire_and_forget ActivateBellIndicatorTimer(); + void ShowBellIndicator(const bool show); + void ActivateBellIndicatorTimer(); float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; std::optional PreCalculateCanSplit(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, @@ -58,7 +58,7 @@ namespace winrt::TerminalApp::implementation bool FocusPane(const uint32_t id); void UpdateSettings(); - winrt::fire_and_forget UpdateTitle(); + void UpdateTitle(); void Shutdown() override; void ClosePane(); @@ -155,7 +155,7 @@ namespace winrt::TerminalApp::implementation void _MakeTabViewItem() override; - winrt::fire_and_forget _UpdateHeaderControlMaxWidth(); + void _UpdateHeaderControlMaxWidth(); void _CreateContextMenu() override; virtual winrt::hstring _CreateToolTipTitle() override; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 12c7ca25416..587745aa9b4 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -961,11 +961,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto bufferHeight = _core.BufferHeight(); - ScrollBar().Maximum(bufferHeight - bufferHeight); + ScrollBar().Maximum(0); ScrollBar().Minimum(0); ScrollBar().Value(0); ScrollBar().ViewportSize(bufferHeight); - ScrollBar().LargeChange(std::max(bufferHeight - 1, 0)); // scroll one "screenful" at a time when the scroll bar is clicked + ScrollBar().LargeChange(bufferHeight); // scroll one "screenful" at a time when the scroll bar is clicked // Set up blinking cursor int blinkTime = GetCaretBlinkTime(); diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index 58b5c8b08d0..ff74b716786 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -72,14 +72,21 @@ latest ControlsV2 version of the template can be found at: https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/CommonStyles/ScrollBar_themeresources.xaml#L218 - We're also removing the corner radius, cause that should be flush + Additionally we have: + * removed the corner radius, because that should be flush with the top of the window above the TermControl. + * set ScrollBarExpandBeginTime to 0 so that the scrollbar, fades in instantly when it's expanded. + This makes it feel much better with cursors compared to the questionable standard 400ms delay in + the Win11-style WinUI ScrollView. If you also have the "Always show scrollbars" setting enabled in + the settings app (do it if you haven't already), it avoids any and all animations during startup which + makes the app start feel noticeably better and also shaves off another ~167ms of our "busy time". We're also planning on making this adjustable in the future (GH#9218), where we might need this anyways. --> 16 + 0 + + + +
+
+ + +
+ + + + + + + + + + + + + + + + +
InputWCAG21:
APCA:
ΔE2000
(ConEmu)
WCAG21:
APCA:
ΔEOKWCAG21:
APCA:
+
+
+

Input

+
+
+
+
+
+
+
+
+
+

ΔE2000 (ConEmu)

+
+
+
+
+
+
+
+
+
+

ΔEOK

+
+
+
+
+
+
+
+
+ + + + diff --git a/src/buffer/out/TextColor.cpp b/src/buffer/out/TextColor.cpp index 7440cc03d36..2e2e6f9fa0b 100644 --- a/src/buffer/out/TextColor.cpp +++ b/src/buffer/out/TextColor.cpp @@ -64,7 +64,7 @@ bool TextColor::CanBeBrightened() const noexcept bool TextColor::IsLegacy() const noexcept { - return IsIndex16() || (IsIndex256() && _index < 16); + return (IsIndex16() || IsIndex256()) && _index < 16; } bool TextColor::IsIndex16() const noexcept @@ -82,6 +82,11 @@ bool TextColor::IsDefault() const noexcept return _meta == ColorType::IsDefault; } +bool TextColor::IsDefaultOrLegacy() const noexcept +{ + return _meta != ColorType::IsRgb && _index < 16; +} + bool TextColor::IsRgb() const noexcept { return _meta == ColorType::IsRgb; diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h index b975028760c..0b71078fbf1 100644 --- a/src/buffer/out/TextColor.h +++ b/src/buffer/out/TextColor.h @@ -37,12 +37,14 @@ Revision History: #include "WexTestClass.h" #endif +// The enum values being in this particular order allows the compiler to do some useful optimizations, +// like simplifying `IsIndex16() || IsIndex256()` into a simple range check without branching. enum class ColorType : BYTE { - IsIndex256 = 0x0, - IsIndex16 = 0x1, - IsDefault = 0x2, - IsRgb = 0x3 + IsDefault, + IsIndex16, + IsIndex256, + IsRgb }; enum class ColorAlias : size_t @@ -121,6 +123,7 @@ struct TextColor bool IsIndex16() const noexcept; bool IsIndex256() const noexcept; bool IsDefault() const noexcept; + bool IsDefaultOrLegacy() const noexcept; bool IsRgb() const noexcept; void SetColor(const COLORREF rgbColor) noexcept; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 99fa8453472..c6fa30d0026 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -180,7 +180,6 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance) { _renderSettings.SetColorTableEntry(i, til::color{ appearance.GetColorTableEntry(i) }); } - _renderSettings.MakeAdjustedColorArray(); auto cursorShape = CursorType::VerticalBar; switch (appearance.CursorShape()) @@ -1325,8 +1324,6 @@ void Terminal::ApplyScheme(const Scheme& colorScheme) _renderSettings.SetColorTableEntry(TextColor::CURSOR_COLOR, til::color{ colorScheme.CursorColor }); - _renderSettings.MakeAdjustedColorArray(); - // Tell the control that the scrollbar has somehow changed. Used as a // workaround to force the control to redraw any scrollbar marks whose color // may have changed. diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 9ecf9beac7e..c4959521fad 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -10,6 +10,7 @@ #include #include "dwrite.h" +#include "../../types/inc/ColorFix.hpp" #if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS #include "colorbrewer.h" @@ -826,7 +827,7 @@ void BackendD3D::_flushQuads(const RenderingPayload& p) if (!_cursorRects.empty()) { - _drawCursorForeground(p); + _drawCursorForeground(); } // TODO: Shrink instances buffer @@ -1627,9 +1628,19 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p) static_cast(p.s->font->cellSize.x * (x1 - x0)), p.s->font->cellSize.y, }; - const auto isInverted = cursorColor == 0xffffffff; - const auto background = isInverted ? bg ^ 0xc0c0c0 : cursorColor; - const auto foreground = isInverted ? 0 : bg; + auto background = cursorColor; + auto foreground = bg; + + if (cursorColor == 0xffffffff) + { + background = bg ^ 0xffffff; + foreground = 0xffffffff; + } + + // The legacy console used to invert colors by just doing `bg ^ 0xc0c0c0`. This resulted + // in a minimum squared distance of just 0.029195 across all possible color combinations. + background = ColorFix::GetPerceivableColor(background, bg, 0.25f * 0.25f); + auto& c0 = _cursorRects.emplace_back(position, size, background, foreground); switch (static_cast(p.s->cursor->cursorType)) @@ -1702,7 +1713,7 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p) } } -void BackendD3D::_drawCursorForeground(const RenderingPayload& p) +void BackendD3D::_drawCursorForeground() { // NOTE: _appendQuad() may reallocate the _instances vector. It's important to iterate // by index, because pointers (or iterators) would get invalidated. It's also important @@ -1779,7 +1790,7 @@ void BackendD3D::_drawCursorForeground(const RenderingPayload& p) { // The _instances vector is _huge_ (easily up to 50k items) whereas only 1-2 items will actually overlap // with the cursor. --> Make this loop more compact by putting as much as possible into a function call. - const auto added = _drawCursorForegroundSlowPath(p, c, i); + const auto added = _drawCursorForegroundSlowPath(c, i); i += added; instancesCount += added; } @@ -1787,7 +1798,7 @@ void BackendD3D::_drawCursorForeground(const RenderingPayload& p) } } -size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, const CursorRect& c, size_t offset) +size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t offset) { // We won't die from copying 24 bytes. It simplifies the code below especially in // respect to when/if we overwrite the _instances[offset] slot with a cutout. @@ -1886,7 +1897,9 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, cons target.color = it.color; } - const auto cursorColor = p.s->cursor->cursorColor; + auto color = c.foreground == 0xffffffff ? it.color ^ 0xffffff : c.foreground; + color = ColorFix::GetPerceivableColor(color, c.background, 0.5f * 0.5f); + // If the cursor covers the entire glyph (like, let's say, a full-box cursor with an ASCII character), // we don't append a new quad, but rather reuse the one that already exists (cutoutCount == 0). auto& target = cutoutCount ? _appendQuad() : _instances[offset]; @@ -1898,7 +1911,7 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, cons target.size.y = static_cast(intersectionB - intersectionT); target.texcoord.x = static_cast(it.texcoord.x + intersectionL - instanceL); target.texcoord.y = static_cast(it.texcoord.y + intersectionT - instanceT); - target.color = cursorColor == 0xffffffff ? it.color ^ 0xc0c0c0 : c.foreground; + target.color = color; return addedInstances; } diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index c5cf07e6553..f75ae954079 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -220,8 +220,8 @@ namespace Microsoft::Console::Render::Atlas void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); void _drawGridlines(const RenderingPayload& p, u16 y); void _drawCursorBackground(const RenderingPayload& p); - ATLAS_ATTR_COLD void _drawCursorForeground(const RenderingPayload& p); - ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const RenderingPayload& p, const CursorRect& c, size_t offset); + ATLAS_ATTR_COLD void _drawCursorForeground(); + ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const CursorRect& c, size_t offset); void _drawSelection(const RenderingPayload& p); void _executeCustomShader(RenderingPayload& p); diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp index 65a63a707dc..03c4795635a 100644 --- a/src/renderer/base/RenderSettings.cpp +++ b/src/renderer/base/RenderSettings.cpp @@ -11,10 +11,6 @@ using namespace Microsoft::Console::Render; using Microsoft::Console::Utils::InitializeColorTable; -static constexpr size_t AdjustedFgIndex{ 16 }; -static constexpr size_t AdjustedBgIndex{ 17 }; -static constexpr size_t AdjustedBrightFgIndex{ 18 }; - RenderSettings::RenderSettings() noexcept { InitializeColorTable(_colorTable); @@ -71,42 +67,6 @@ void RenderSettings::ResetColorTable() noexcept InitializeColorTable({ _colorTable.data(), 16 }); } -// Routine Description: -// - Creates the adjusted color array, which contains the possible foreground colors, -// adjusted for perceivability -// - The adjusted color array is 2-d, and effectively maps a background and foreground -// color pair to the adjusted foreground for that color pair -void RenderSettings::MakeAdjustedColorArray() noexcept -{ - // The color table has 16 colors, but the adjusted color table needs to be 19 - // to include the default background, default foreground and bright default foreground colors - std::array colorTableWithDefaults; - std::copy_n(std::begin(_colorTable), 16, std::begin(colorTableWithDefaults)); - colorTableWithDefaults[AdjustedFgIndex] = GetColorAlias(ColorAlias::DefaultForeground); - colorTableWithDefaults[AdjustedBgIndex] = GetColorAlias(ColorAlias::DefaultBackground); - - // We need to use TextColor to calculate the bright default fg - TextColor defaultFg; - colorTableWithDefaults[AdjustedBrightFgIndex] = defaultFg.GetColor(_colorTable, GetColorAliasIndex(ColorAlias::DefaultForeground), true); - - for (auto fgIndex = 0; fgIndex < 19; ++fgIndex) - { - const auto fg = til::at(colorTableWithDefaults, fgIndex); - for (auto bgIndex = 0; bgIndex < 19; ++bgIndex) - { - if (fgIndex == bgIndex) - { - _adjustedForegroundColors[bgIndex][fgIndex] = fg; - } - else - { - const auto bg = til::at(colorTableWithDefaults, bgIndex); - _adjustedForegroundColors[bgIndex][fgIndex] = ColorFix::GetPerceivableColor(fg, bg); - } - } - } -} - // Routine Description: // - Updates the given index in the color table to a new value. // Arguments: @@ -196,74 +156,37 @@ std::pair RenderSettings::GetAttributeColors(const TextAttri const auto dimFg = attr.IsFaint() || (_blinkShouldBeFaint && attr.IsBlinking()); const auto swapFgAndBg = attr.IsReverseVideo() ^ GetRenderMode(Mode::ScreenReversed); - // We want to nudge the foreground color to make it more perceivable only for the - // default color pairs within the color table - if (Feature_AdjustIndistinguishableText::IsEnabled() && - GetRenderMode(Mode::IndexedDistinguishableColors) && - !dimFg && - !attr.IsInvisible() && - (fgTextColor.IsDefault() || fgTextColor.IsLegacy()) && - (bgTextColor.IsDefault() || bgTextColor.IsLegacy())) - { - const auto bgIndex = bgTextColor.IsDefault() ? AdjustedBgIndex : bgTextColor.GetIndex(); - auto fgIndex = fgTextColor.IsDefault() ? AdjustedFgIndex : fgTextColor.GetIndex(); - - if (brightenFg) - { - // There is a special case for intense here - we need to get the bright version of the foreground color - if (fgTextColor.IsIndex16() && (fgIndex < 8)) - { - fgIndex += 8; - } - else if (fgTextColor.IsDefault()) - { - fgIndex = AdjustedBrightFgIndex; - } - } + auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); + auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); - if (swapFgAndBg) - { - const auto fg = _adjustedForegroundColors[fgIndex][bgIndex]; - const auto bg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); - return { fg, bg }; - } - else - { - const auto fg = _adjustedForegroundColors[bgIndex][fgIndex]; - const auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); - return { fg, bg }; - } + if (dimFg) + { + fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two. } - else + if (swapFgAndBg) { - auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); - auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); - - if (dimFg) - { - fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two. - } - if (swapFgAndBg) - { - std::swap(fg, bg); - } - if (attr.IsInvisible()) - { - fg = bg; - } + std::swap(fg, bg); + } + if (attr.IsInvisible()) + { + fg = bg; + } - // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to - // catch the cases where the fg was intentionally set to be the same as the bg. In either case, - // don't adjust the foreground. - if (Feature_AdjustIndistinguishableText::IsEnabled() && + // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to + // catch the cases where the fg was intentionally set to be the same as the bg. In either case, + // don't adjust the foreground. + if constexpr (Feature_AdjustIndistinguishableText::IsEnabled()) + { + if ( + _renderMode.any(Mode::IndexedDistinguishableColors, Mode::AlwaysDistinguishableColors) && fg != bg && - GetRenderMode(Mode::AlwaysDistinguishableColors)) + (_renderMode.test(Mode::AlwaysDistinguishableColors) || (fgTextColor.IsDefaultOrLegacy() && bgTextColor.IsDefaultOrLegacy()))) { - fg = ColorFix::GetPerceivableColor(fg, bg); + fg = ColorFix::GetPerceivableColor(fg, bg, 0.5f * 0.5f); } - - return { fg, bg }; } + + return { fg, bg }; } // Routine Description: diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp index 897d8e099d4..4b6e7c3981c 100644 --- a/src/renderer/inc/RenderSettings.hpp +++ b/src/renderer/inc/RenderSettings.hpp @@ -33,7 +33,6 @@ namespace Microsoft::Console::Render bool GetRenderMode(const Mode mode) const noexcept; const std::array& GetColorTable() const noexcept; void ResetColorTable() noexcept; - void MakeAdjustedColorArray() noexcept; void SetColorTableEntry(const size_t tableIndex, const COLORREF color); COLORREF GetColorTableEntry(const size_t tableIndex) const; void SetColorAlias(const ColorAlias alias, const size_t tableIndex, const COLORREF color); @@ -48,7 +47,6 @@ namespace Microsoft::Console::Render til::enumset _renderMode{ Mode::BlinkAllowed, Mode::IntenseIsBright }; std::array _colorTable; std::array(ColorAlias::ENUM_COUNT)> _colorAliasIndices; - std::array, 19> _adjustedForegroundColors; size_t _blinkCycle = 0; mutable bool _blinkIsInUse = false; bool _blinkShouldBeFaint = false; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 6a4d7b8d672..7c2c04705fd 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2940,11 +2940,6 @@ bool AdaptDispatch::SetClipboard(const std::wstring_view content) bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) { _renderSettings.SetColorTableEntry(tableIndex, dwColor); - if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors)) - { - // Re-calculate the adjusted colors now that one of the entries has been changed - _renderSettings.MakeAdjustedColorArray(); - } // If we're a conpty, always return false, so that we send the updated color // value to the terminal. Still handle the sequence so apps that use @@ -3010,11 +3005,6 @@ bool AdaptDispatch::AssignColor(const DispatchTypes::ColorItem item, const VTInt case DispatchTypes::ColorItem::NormalText: _renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, fgIndex); _renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, bgIndex); - if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors)) - { - // Re-calculate the adjusted colors now that these aliases have been changed - _renderSettings.MakeAdjustedColorArray(); - } break; case DispatchTypes::ColorItem::WindowFrame: _renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, fgIndex); diff --git a/src/types/ColorFix.cpp b/src/types/ColorFix.cpp index 723d7059f68..eda47d396a2 100644 --- a/src/types/ColorFix.cpp +++ b/src/types/ColorFix.cpp @@ -1,231 +1,212 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// A lot of code was taken from -// https://github.com/Maximus5/ConEmu/blob/master/src/ConEmu/ColorFix.cpp -// and then adjusted to fit our style guidelines - #include "precomp.h" - #include "inc/ColorFix.hpp" -TIL_FAST_MATH_BEGIN - -static constexpr float gMinThreshold = 12.0f; -static constexpr float gExpThreshold = 20.0f; -static constexpr float gLStep = 5.0f; +// This table contains a direct mapping from 8-bit sRGB to linear RGB. +// It was generated using the following code: +// #include +// +// int main() { +// char buffer[32]; +// +// for (int i = 0; i < 256; ++i) { +// auto v = i / 255.0f; +// v = v <= 0.04045f ? v / 12.92f : powf((v + 0.055f) / 1.055f, 2.4f); +// +// auto p = std::to_chars(&buffer[0], &buffer[32], v, std::chars_format::hex, -1).ptr; +// strcpy_s(p, &buffer[32] - p, "f,"); +// +// if (i % 16 == 0) printf("\n "); +// printf("0x%-15s", &buffer[0]); +// } +// +// return 0; +// } +// +// clang-format off +static constexpr float srgbToRgbLUT[256]{ + 0x0.000000p+0f, 0x1.3e4568p-12f, 0x1.3e4568p-11f, 0x1.dd681cp-11f, 0x1.3e4568p-10f, 0x1.8dd6c2p-10f, 0x1.dd681cp-10f, 0x1.167cbap-9f, 0x1.3e4568p-9f, 0x1.660e16p-9f, 0x1.8dd6c2p-9f, 0x1.b6a31ap-9f, 0x1.e1e31ap-9f, 0x1.07c38cp-8f, 0x1.1fcc2cp-8f, 0x1.390ffap-8f, + 0x1.53936ep-8f, 0x1.6f5adep-8f, 0x1.8c6a92p-8f, 0x1.aac6c2p-8f, 0x1.ca7382p-8f, 0x1.eb74e0p-8f, 0x1.06e76ap-7f, 0x1.18c2a4p-7f, 0x1.2b4e06p-7f, 0x1.3e8b7cp-7f, 0x1.527cd6p-7f, 0x1.6723eep-7f, 0x1.7c8292p-7f, 0x1.929a86p-7f, 0x1.a96d8ep-7f, 0x1.c0fd62p-7f, + 0x1.d94bbep-7f, 0x1.f25a44p-7f, 0x1.061550p-6f, 0x1.135f3ep-6f, 0x1.210bb6p-6f, 0x1.2f1b8ap-6f, 0x1.3d8f84p-6f, 0x1.4c6866p-6f, 0x1.5ba6fap-6f, 0x1.6b4c02p-6f, 0x1.7b5840p-6f, 0x1.8bcc72p-6f, 0x1.9ca956p-6f, 0x1.adefaap-6f, 0x1.bfa020p-6f, 0x1.d1bb72p-6f, + 0x1.e44258p-6f, 0x1.f73582p-6f, 0x1.054ad2p-5f, 0x1.0f31b8p-5f, 0x1.194fccp-5f, 0x1.23a55ep-5f, 0x1.2e32c6p-5f, 0x1.38f85cp-5f, 0x1.43f678p-5f, 0x1.4f2d64p-5f, 0x1.5a9d76p-5f, 0x1.664700p-5f, 0x1.722a56p-5f, 0x1.7e47c6p-5f, 0x1.8a9fa2p-5f, 0x1.973238p-5f, + 0x1.a3ffdep-5f, 0x1.b108d4p-5f, 0x1.be4d6ep-5f, 0x1.cbcdfcp-5f, 0x1.d98ac8p-5f, 0x1.e7841ep-5f, 0x1.f5ba52p-5f, 0x1.0216cep-4f, 0x1.096f2ap-4f, 0x1.10e660p-4f, 0x1.187c94p-4f, 0x1.2031ecp-4f, 0x1.28068ap-4f, 0x1.2ffa94p-4f, 0x1.380e2cp-4f, 0x1.404176p-4f, + 0x1.489496p-4f, 0x1.5107aep-4f, 0x1.599ae0p-4f, 0x1.624e50p-4f, 0x1.6b2224p-4f, 0x1.741676p-4f, 0x1.7d2b6ap-4f, 0x1.866124p-4f, 0x1.8fb7c4p-4f, 0x1.992f6cp-4f, 0x1.a2c83ep-4f, 0x1.ac8258p-4f, 0x1.b65ddep-4f, 0x1.c05aeep-4f, 0x1.ca79aap-4f, 0x1.d4ba32p-4f, + 0x1.df1ca4p-4f, 0x1.e9a122p-4f, 0x1.f447d0p-4f, 0x1.ff10c2p-4f, 0x1.04fe0ep-3f, 0x1.0a8500p-3f, 0x1.101d46p-3f, 0x1.15c6f0p-3f, 0x1.1b820ap-3f, 0x1.214ea8p-3f, 0x1.272cd6p-3f, 0x1.2d1ca4p-3f, 0x1.331e20p-3f, 0x1.39315ap-3f, 0x1.3f5660p-3f, 0x1.458d46p-3f, + 0x1.4bd612p-3f, 0x1.5230d8p-3f, 0x1.589da0p-3f, 0x1.5f1c82p-3f, 0x1.65ad88p-3f, 0x1.6c50c2p-3f, 0x1.73063cp-3f, 0x1.79ce06p-3f, 0x1.80a82cp-3f, 0x1.8794bep-3f, 0x1.8e93c8p-3f, 0x1.95a55cp-3f, 0x1.9cc984p-3f, 0x1.a4004ep-3f, 0x1.ab49ccp-3f, 0x1.b2a606p-3f, + 0x1.ba1516p-3f, 0x1.c196f8p-3f, 0x1.c92bc0p-3f, 0x1.d0d37ep-3f, 0x1.d88e40p-3f, 0x1.e05c18p-3f, 0x1.e83d08p-3f, 0x1.f03120p-3f, 0x1.f83870p-3f, 0x1.002984p-2f, 0x1.044078p-2f, 0x1.08611ap-2f, 0x1.0c8b74p-2f, 0x1.10bf8ap-2f, 0x1.14fd64p-2f, 0x1.194506p-2f, + 0x1.1d967ap-2f, 0x1.21f1c2p-2f, 0x1.2656e8p-2f, 0x1.2ac5f0p-2f, 0x1.2f3ee2p-2f, 0x1.33c1c4p-2f, 0x1.384e9cp-2f, 0x1.3ce56ep-2f, 0x1.418644p-2f, 0x1.463124p-2f, 0x1.4ae610p-2f, 0x1.4fa512p-2f, 0x1.546e30p-2f, 0x1.59416ep-2f, 0x1.5e1ed2p-2f, 0x1.630666p-2f, + 0x1.67f830p-2f, 0x1.6cf430p-2f, 0x1.71fa6ep-2f, 0x1.770af2p-2f, 0x1.7c25c2p-2f, 0x1.814ae2p-2f, 0x1.867a5ap-2f, 0x1.8bb42ep-2f, 0x1.90f866p-2f, 0x1.964706p-2f, 0x1.9ba016p-2f, 0x1.a1039ap-2f, 0x1.a67198p-2f, 0x1.abea16p-2f, 0x1.b16d1ap-2f, 0x1.b6faa8p-2f, + 0x1.bc92cap-2f, 0x1.c23582p-2f, 0x1.c7e2d6p-2f, 0x1.cd9acep-2f, 0x1.d35d6cp-2f, 0x1.d92abap-2f, 0x1.df02bap-2f, 0x1.e4e574p-2f, 0x1.ead2ecp-2f, 0x1.f0cb28p-2f, 0x1.f6ce2ep-2f, 0x1.fcdc04p-2f, 0x1.017a5ap-1f, 0x1.048c1cp-1f, 0x1.07a34ep-1f, 0x1.0abff2p-1f, + 0x1.0de20cp-1f, 0x1.11099ep-1f, 0x1.1436acp-1f, 0x1.176936p-1f, 0x1.1aa140p-1f, 0x1.1ddecep-1f, 0x1.2121e2p-1f, 0x1.246a7cp-1f, 0x1.27b8a2p-1f, 0x1.2b0c56p-1f, 0x1.2e659ap-1f, 0x1.31c472p-1f, 0x1.3528dep-1f, 0x1.3892e2p-1f, 0x1.3c0282p-1f, 0x1.3f77bep-1f, + 0x1.42f29cp-1f, 0x1.46731cp-1f, 0x1.49f940p-1f, 0x1.4d850cp-1f, 0x1.511682p-1f, 0x1.54ada6p-1f, 0x1.584a7ap-1f, 0x1.5bed00p-1f, 0x1.5f953cp-1f, 0x1.63432ep-1f, 0x1.66f6d8p-1f, 0x1.6ab040p-1f, 0x1.6e6f66p-1f, 0x1.72344cp-1f, 0x1.75fef8p-1f, 0x1.79cf6ap-1f, + 0x1.7da5a4p-1f, 0x1.8181a8p-1f, 0x1.85637cp-1f, 0x1.894b20p-1f, 0x1.8d3896p-1f, 0x1.912be2p-1f, 0x1.952504p-1f, 0x1.992402p-1f, 0x1.9d28dcp-1f, 0x1.a13396p-1f, 0x1.a54430p-1f, 0x1.a95aaep-1f, 0x1.ad7714p-1f, 0x1.b19960p-1f, 0x1.b5c19ap-1f, 0x1.b9efc0p-1f, + 0x1.be23d8p-1f, 0x1.c25ddcp-1f, 0x1.c69ddep-1f, 0x1.cae3d2p-1f, 0x1.cf2fc6p-1f, 0x1.d381acp-1f, 0x1.d7d99ap-1f, 0x1.dc377ep-1f, 0x1.e09b70p-1f, 0x1.e5055ep-1f, 0x1.e9755cp-1f, 0x1.edeb5cp-1f, 0x1.f26772p-1f, 0x1.f6e98cp-1f, 0x1.fb71c0p-1f, 0x1.000000p+0f, +}; +// clang-format on -static constexpr float rad006 = 0.104719755119659774f; -static constexpr float rad025 = 0.436332312998582394f; -static constexpr float rad030 = 0.523598775598298873f; -static constexpr float rad060 = 1.047197551196597746f; -static constexpr float rad063 = 1.099557428756427633f; -static constexpr float rad180 = 3.141592653589793238f; -static constexpr float rad275 = 4.799655442984406336f; -static constexpr float rad360 = 6.283185307179586476f; +TIL_FAST_MATH_BEGIN -ColorFix::ColorFix(COLORREF color) noexcept +constexpr float saturate(float f) noexcept { - rgb = color; - _ToLab(); + return f < 0 ? 0 : (f > 1 ? 1 : f); } -// Method Description: -// - Helper function to calculate HPrime -float ColorFix::_GetHPrimeFn(float x, float y) noexcept +// FP32 is the very epitome of defined behavior. +#pragma warning(suppress : 26497) // You can attempt to make 'cbrtf_est' constexpr unless it contains any undefined behavior (f.4).) +__forceinline float cbrtf_est(float a) noexcept { - if (x == 0 && y == 0) + // We can't use std::bit_cast here, even though it exists exactly for this purpose. + // MSVC is dumb and introduces function calls to std::bit_cast + // even at the highest optimization and inlining levels. What. + union FP32 { - return 0; - } - - const auto hueAngle = atan2(x, y); - return hueAngle >= 0 ? hueAngle : hueAngle + rad360; + uint32_t u; + float f; + }; + + // http://metamerist.com/cbrt/cbrt.htm showed a great estimator for the cube root: + // float_as_uint32_t / 3 + 709921077 + // It's similar to the well known "fast inverse square root" trick. Lots of numbers around 709921077 perform + // at least equally well to 709921077, and it is unknown how and why 709921077 was chosen specifically. + FP32 fp{ .f = a }; // evil floating point bit level hacking + fp.u = fp.u / 3 + 709921077; // what the fuck? + const auto x = fp.f; + + // One round of Newton's method. It follows the Wikipedia article at + // https://en.wikipedia.org/wiki/Cube_root#Numerical_methods + // For `a`s in the range between 0 and 1, this results in a maximum error of + // less than 6.7e-4f, which is not good, but good enough for us, because + // we're not an image editor. The benefit is that it's really fast. + return (1.0f / 3.0f) * (a / (x * x) + (x + x)); // 1st iteration } -// Method Description: -// - Given 2 colors, computes the DeltaE value between them -// Arguments: -// - x1: the first color -// - x2: the second color -// Return Value: -// - The DeltaE value between x1 and x2 -float ColorFix::_GetDeltaE(ColorFix x1, ColorFix x2) noexcept +// This namespace contains functions and structures as defined by: https://bottosson.github.io/posts/oklab/ +// It has been released into the public domain and is also available under an MIT license. +// The only change I made is to replace cbrtf() with cbrtf_est() to cut the CPU cost by a third. +namespace oklab { - constexpr float kSubL = 1; - constexpr float kSubC = 1; - constexpr float kSubH = 1; - - // Delta L Prime - const auto deltaLPrime = x2.L - x1.L; - - // L Bar - const auto lBar = (x1.L + x2.L) / 2; - - // C1 & C2 - const auto c1 = sqrtf(powf(x1.A, 2) + powf(x1.B, 2)); - const auto c2 = sqrtf(powf(x2.A, 2) + powf(x2.B, 2)); - - // C Bar - const auto cBar = (c1 + c2) / 2; - - // A Prime 1 - const auto aPrime1 = x1.A + (x1.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7)))); - - // A Prime 2 - const auto aPrime2 = x2.A + (x2.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7)))); - - // C Prime 1 - const auto cPrime1 = sqrtf(powf(aPrime1, 2) + powf(x1.B, 2)); - - // C Prime 2 - const auto cPrime2 = sqrtf(powf(aPrime2, 2) + powf(x2.B, 2)); - - // C Bar Prime - const auto cBarPrime = (cPrime1 + cPrime2) / 2; - - // Delta C Prime - const auto deltaCPrime = cPrime2 - cPrime1; - - // S sub L - const auto sSubL = 1 + ((0.015f * powf(lBar - 50, 2)) / sqrtf(20 + powf(lBar - 50, 2))); - - // S sub C - const auto sSubC = 1 + 0.045f * cBarPrime; - - // h Prime 1 - const auto hPrime1 = _GetHPrimeFn(x1.B, aPrime1); - - // h Prime 2 - const auto hPrime2 = _GetHPrimeFn(x2.B, aPrime2); - - // Delta H Prime - const auto deltaHPrime = 0 == c1 || 0 == c2 ? 0 : 2 * sqrtf(cPrime1 * cPrime2) * sin(abs(hPrime1 - hPrime2) <= rad180 ? hPrime2 - hPrime1 : (hPrime2 <= hPrime1 ? hPrime2 - hPrime1 + rad360 : hPrime2 - hPrime1 - rad360) / 2); - - // H Bar Prime - const auto hBarPrime = (abs(hPrime1 - hPrime2) > rad180) ? (hPrime1 + hPrime2 + rad360) / 2 : (hPrime1 + hPrime2) / 2; + struct Lab + { + float l; + float a; + float b; + }; - // T - const auto t = 1 - 0.17f * cosf(hBarPrime - rad030) + 0.24f * cosf(2 * hBarPrime) + 0.32f * cosf(3 * hBarPrime + rad006) - 0.20f * cosf(4 * hBarPrime - rad063); + struct RGB + { + float r; + float g; + float b; + }; - // S sub H - const auto sSubH = 1 + 0.015f * cBarPrime * t; + __forceinline Lab linear_srgb_to_oklab(const RGB& c) noexcept + { + const auto l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b; + const auto m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b; + const auto s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b; + + const auto l_ = cbrtf_est(l); + const auto m_ = cbrtf_est(m); + const auto s_ = cbrtf_est(s); + + return { + 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_, + 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_, + 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_, + }; + } - // R sub T - const auto rSubT = -2 * sqrtf(powf(cBarPrime, 7) / (powf(cBarPrime, 7) + powf(25.0f, 7))) * sin(rad060 * exp(-powf((hBarPrime - rad275) / rad025, 2))); + __forceinline RGB oklab_to_linear_srgb(const Lab& c) noexcept + { + const auto l_ = c.l + 0.3963377774f * c.a + 0.2158037573f * c.b; + const auto m_ = c.l - 0.1055613458f * c.a - 0.0638541728f * c.b; + const auto s_ = c.l - 0.0894841775f * c.a - 1.2914855480f * c.b; + + const auto l = l_ * l_ * l_; + const auto m = m_ * m_ * m_; + const auto s = s_ * s_ * s_; + + return { + +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s, + -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s, + -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s, + }; + } +} - // Put it all together! - const auto lightness = deltaLPrime / (kSubL * sSubL); - const auto chroma = deltaCPrime / (kSubC * sSubC); - const auto hue = deltaHPrime / (kSubH * sSubH); +#pragma warning(push) +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). - return sqrtf(powf(lightness, 2) + powf(chroma, 2) + powf(hue, 2) + rSubT * chroma * hue); +__forceinline oklab::RGB colorrefToLinear(COLORREF c) noexcept +{ + const auto r = srgbToRgbLUT[(c >> 0) & 0xff]; + const auto g = srgbToRgbLUT[(c >> 8) & 0xff]; + const auto b = srgbToRgbLUT[(c >> 16) & 0xff]; + return { r, g, b }; } -// Method Description: -// - Populates our L, A, B values, based on our r, g, b values -// - Converts a color in rgb format to a color in lab format -// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1 -void ColorFix::_ToLab() noexcept +#pragma warning(pop) + +__forceinline COLORREF linearToColorref(const oklab::RGB& c) noexcept { - auto var_R = r / 255.0f; - auto var_G = g / 255.0f; - auto var_B = b / 255.0f; - - var_R = var_R > 0.04045f ? powf(((var_R + 0.055f) / 1.055f), 2.4f) : var_R / 12.92f; - var_G = var_G > 0.04045f ? powf(((var_G + 0.055f) / 1.055f), 2.4f) : var_G / 12.92f; - var_B = var_B > 0.04045f ? powf(((var_B + 0.055f) / 1.055f), 2.4f) : var_B / 12.92f; - - var_R = var_R * 100.0f; - var_G = var_G * 100.0f; - var_B = var_B * 100.0f; - - //Observer. = 2 degrees, Illuminant = D65 - const auto X = var_R * 0.4124f + var_G * 0.3576f + var_B * 0.1805f; - const auto Y = var_R * 0.2126f + var_G * 0.7152f + var_B * 0.0722f; - const auto Z = var_R * 0.0193f + var_G * 0.1192f + var_B * 0.9505f; - - auto var_X = X / 95.047f; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65) - auto var_Y = Y / 100.000f; //ref_Y = 100.000 - auto var_Z = Z / 108.883f; //ref_Z = 108.883 - - var_X = var_X > 0.008856f ? powf(var_X, (1.0f / 3.0f)) : (7.787f * var_X) + (16.0f / 116.0f); - var_Y = var_Y > 0.008856f ? powf(var_Y, (1.0f / 3.0f)) : (7.787f * var_Y) + (16.0f / 116.0f); - var_Z = var_Z > 0.008856f ? powf(var_Z, (1.0f / 3.0f)) : (7.787f * var_Z) + (16.0f / 116.0f); - - L = (116.0f * var_Y) - 16.0f; - A = 500.0f * (var_X - var_Y); - B = 200.0f * (var_Y - var_Z); + auto r = saturate(c.r); + auto g = saturate(c.g); + auto b = saturate(c.b); + r = (r > 0.0031308f ? (1.055f * powf(r, 1.0f / 2.4f) - 0.055f) : 12.92f * r) * 255.0f; + g = (g > 0.0031308f ? (1.055f * powf(g, 1.0f / 2.4f) - 0.055f) : 12.92f * g) * 255.0f; + b = (b > 0.0031308f ? (1.055f * powf(b, 1.0f / 2.4f) - 0.055f) : 12.92f * b) * 255.0f; + return lrintf(r) | (lrintf(g) << 8) | (lrintf(b) << 16); } -// Method Description: -// - Populates our r, g, b values, based on our L, A, B values -// - Converts a color in lab format to a color in rgb format -// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1 -void ColorFix::_ToRGB() +// This function changes `color` so that it is visually different +// enough from `reference` that it's (much more easily) readable. +// See /doc/color_nudging.html +COLORREF ColorFix::GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept { - auto var_Y = (L + 16.0f) / 116.0f; - auto var_X = A / 500.0f + var_Y; - auto var_Z = var_Y - B / 200.0f; - - var_Y = (powf(var_Y, 3) > 0.008856f) ? powf(var_Y, 3) : (var_Y - 16.0f / 116.0f) / 7.787f; - var_X = (powf(var_X, 3) > 0.008856f) ? powf(var_X, 3) : (var_X - 16.0f / 116.0f) / 7.787f; - var_Z = (powf(var_Z, 3) > 0.008856f) ? powf(var_Z, 3) : (var_Z - 16.0f / 116.0f) / 7.787f; - - const auto X = 95.047f * var_X; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65) - const auto Y = 100.000f * var_Y; //ref_Y = 100.000 - const auto Z = 108.883f * var_Z; //ref_Z = 108.883 - - var_X = X / 100.0f; //X from 0 to 95.047 (Observer = 2 degrees, Illuminant = D65) - var_Y = Y / 100.0f; //Y from 0 to 100.000 - var_Z = Z / 100.0f; //Z from 0 to 108.883 - - auto var_R = var_X * 3.2406f + var_Y * -1.5372f + var_Z * -0.4986f; - auto var_G = var_X * -0.9689f + var_Y * 1.8758f + var_Z * 0.0415f; - auto var_B = var_X * 0.0557f + var_Y * -0.2040f + var_Z * 1.0570f; + const auto referenceOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(reference)); + auto colorOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(color)); + + // To determine whether the two colors are too close to each other we use the ΔEOK metric + // based on the Oklab color space. It's defined as the simple euclidean distance between. + auto dl = referenceOklab.l - colorOklab.l; + auto da = referenceOklab.a - colorOklab.a; + auto db = referenceOklab.b - colorOklab.b; + dl *= dl; + da *= da; + db *= db; + + const auto distance = dl + da + db; + if (distance >= minSquaredDistance) + { + return color; + } - var_R = var_R > 0.0031308f ? 1.055f * powf(var_R, (1 / 2.4f)) - 0.055f : var_R = 12.92f * var_R; - var_G = var_G > 0.0031308f ? 1.055f * powf(var_G, (1 / 2.4f)) - 0.055f : var_G = 12.92f * var_G; - var_B = var_B > 0.0031308f ? 1.055f * powf(var_B, (1 / 2.4f)) - 0.055f : var_B = 12.92f * var_B; + // Thanks to ΔEOK being the euclidean distance we can immediately compute the + // minimum .l distance that's required for `distance` to be >= `minSquaredDistance`. + auto deltaL = sqrtf(minSquaredDistance - da - db); - r = (BYTE)std::clamp(var_R * 255.0f, 0.0f, 255.0f); - g = (BYTE)std::clamp(var_G * 255.0f, 0.0f, 255.0f); - b = (BYTE)std::clamp(var_B * 255.0f, 0.0f, 255.0f); -} + // Try to retain the brightness relationship between `reference` and `color`. + // If `color` is darker than `reference`, we should first try to make it even darker. + if (colorOklab.l < referenceOklab.l) + { + deltaL = -deltaL; + } -// Method Description: -// - Given foreground and background colors, change the foreground color to -// make it more perceivable if necessary -// - Arguments: -// - fg: the foreground color -// - bg: the background color -// - Return Value: -// - The foreground color after performing any necessary changes to make it more perceivable -COLORREF ColorFix::GetPerceivableColor(COLORREF fg, COLORREF bg) -{ - const ColorFix backLab(bg); - ColorFix frontLab(fg); - const auto de1 = _GetDeltaE(frontLab, backLab); - if (de1 < gMinThreshold) + // This primitive way of adjusting the lightness will result in gamut clipping. That's really not good + // (it reduces the contrast significantly for some colors), but on the other hand, doing proper gamut + // mapping is annoying and expensive. I was unable to find an easy and cheap (!) algorithm that would + // change the chroma so that we're (mostly) back inside the gamut, but found none. I'm sure it's + // possible to come up with something, but I left that as a future improvement. + colorOklab.l = referenceOklab.l + deltaL; + if (colorOklab.l < 0 || colorOklab.l > 1) { - for (auto i = 0; i <= 1; i++) - { - const auto step = (i == 0) ? gLStep : -gLStep; - frontLab.L += step; - - while (((i == 0) && (frontLab.L <= 100)) || ((i == 1) && (frontLab.L >= 0))) - { - const auto de2 = _GetDeltaE(frontLab, backLab); - if (de2 >= gExpThreshold) - { - frontLab._ToRGB(); - return frontLab.rgb; - } - frontLab.L += step; - } - } + colorOklab.l = referenceOklab.l - deltaL; } - return frontLab.rgb; + + return linearToColorref(oklab::oklab_to_linear_srgb(colorOklab)) | (color & 0xff000000); } TIL_FAST_MATH_END diff --git a/src/types/inc/ColorFix.hpp b/src/types/inc/ColorFix.hpp index a233d1268aa..02bdc0631b3 100644 --- a/src/types/inc/ColorFix.hpp +++ b/src/types/inc/ColorFix.hpp @@ -1,53 +1,13 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- ColorFix - -Abstract: -- Implementation of perceptual color nudging, which allows the Terminal - to slightly shift the foreground color to make it more perceivable on - the current background (for cases where the foreground is very close - to being imperceivable on the background). - -Author(s): -- Pankaj Bhojwani - Sep 2021 - ---*/ - +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. +// +// Implementation of perceptual color nudging, which allows the Terminal +// to slightly shift the foreground color to make it more perceivable on +// the current background (for cases where the foreground is very close +// to being imperceivable on the background). #pragma once -struct ColorFix +namespace ColorFix { -public: - ColorFix(COLORREF color) noexcept; - - static COLORREF GetPerceivableColor(COLORREF fg, COLORREF bg); - -#pragma warning(push) - // CL will complain about the both nameless and anonymous struct. -#pragma warning(disable : 4201) - // RGB - union - { - struct - { - BYTE r, g, b, dummy; - }; - COLORREF rgb; - }; - - // Lab - struct - { - float L, A, B; - }; -#pragma warning(pop) - -private: - static float _GetHPrimeFn(float x, float y) noexcept; - static float _GetDeltaE(ColorFix x1, ColorFix x2) noexcept; - void _ToLab() noexcept; - void _ToRGB(); -}; + COLORREF GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept; +} From 6ad8cd0a630ab927629841a14d433c3bc19a1509 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 10 May 2023 07:16:44 -0500 Subject: [PATCH 092/226] Make conhost act in VtIo mode earlier in startup (#15298) We need to act like a ConPTY just a little earlier in startup. My relevant notes start here: https://github.com/microsoft/terminal/issues/15245#issuecomment-1536150388. Basically, we'd create the first screen buffer with 9001 rows, because it would be created _before_ VtIo would be in a state to say "yes, we're a conpty". Then, if a CLI app emits an entire screenful of text _before_ the terminal has a chance to resize the conpty, then the conpty will explode during `_DoLineFeed`. That method is absolutely not expecting the buffer to get resized (and the old text buffer deallocated). Instead, this will treat the console as in ConPty mode as soon as `VtIo::Initialize` is called (this is during `ConsoleCreateIoThread`, which is right at the end of `ConsoleEstablishHandoff`, which is before the API server starts to process the client connect message). THEORETICALLY, `VtIo` could `Initialize` then fail to create objects in `CreateIoHandlers` (which is what we used to treat as the moment that we were in conpty mode). However, if we do fail out of `CreateIoHandlers`, then the console itself will fail to start up, and just die. So I don't think that's needed. This fixes #15245. I think this is PROBABLY also the solution to #14512, but I'm not gonna explicitly mark closed. We'll loop back on it. --- src/host/VtIo.cpp | 8 +++----- src/host/VtIo.hpp | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index f3a4b0228d0..b0f4ebad2c3 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -25,7 +25,6 @@ using namespace Microsoft::Console::Interactivity; VtIo::VtIo() : _initialized(false), - _objectsCreated(false), _lookingForCursorPosition(false), _IoMode(VtIoMode::INVALID) { @@ -224,13 +223,12 @@ VtIo::VtIo() : } CATCH_RETURN(); - _objectsCreated = true; return S_OK; } bool VtIo::IsUsingVt() const { - return _objectsCreated; + return _initialized; } // Routine Description: @@ -246,7 +244,7 @@ bool VtIo::IsUsingVt() const [[nodiscard]] HRESULT VtIo::StartIfNeeded() { // If we haven't been set up, do nothing (because there's nothing to start) - if (!_objectsCreated) + if (!_initialized) { return S_FALSE; } @@ -514,7 +512,7 @@ void VtIo::EndResize() // - void VtIo::EnableConptyModeForTests(std::unique_ptr vtRenderEngine) { - _objectsCreated = true; + _initialized = true; _pVtRenderEngine = std::move(vtRenderEngine); } #endif diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp index b3661f59d43..e47d37d9ef6 100644 --- a/src/host/VtIo.hpp +++ b/src/host/VtIo.hpp @@ -63,7 +63,6 @@ namespace Microsoft::Console::VirtualTerminal VtIoMode _IoMode; bool _initialized; - bool _objectsCreated; bool _lookingForCursorPosition; From 48eee4d75aef934e97f7831cbcc3335de35c5279 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 10 May 2023 13:04:41 -0500 Subject: [PATCH 093/226] Update MUX to 2.8.4 (#15313) Reverts #15164, because that's fixed upstream now. Closes #15139. Reverts #15178, but also closes #15121, because that's fixed upstream. see also: * https://github.com/microsoft/microsoft-ui-xaml/pull/8430 * https://github.com/microsoft/microsoft-ui-xaml/pull/8420 --- dep/nuget/packages.config | 2 +- .../TerminalApp/MinMaxCloseControl.xaml | 2 +- src/cascadia/TerminalApp/TabManagement.cpp | 53 +++++++++++-------- src/cascadia/TerminalApp/TabRowControl.xaml | 1 - src/cascadia/TerminalApp/TerminalPage.cpp | 7 ++- src/cascadia/TerminalApp/TerminalPage.h | 5 +- src/common.nugetversions.props | 2 +- src/common.nugetversions.targets | 4 +- 8 files changed, 41 insertions(+), 35 deletions(-) diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index 2e4323f6912..fb1f261b593 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -7,7 +7,7 @@ - + diff --git a/src/cascadia/TerminalApp/MinMaxCloseControl.xaml b/src/cascadia/TerminalApp/MinMaxCloseControl.xaml index 4bc516bc313..fabbb0757a4 100644 --- a/src/cascadia/TerminalApp/MinMaxCloseControl.xaml +++ b/src/cascadia/TerminalApp/MinMaxCloseControl.xaml @@ -146,7 +146,7 @@ --> 40.0 - 32.0 + 33.0