Merged PR 5704731: Migrate OSS up to e6aa90222

Related work items: MSFT-9667854, MSFT-31783834
This commit is contained in:
Dustin Howett 2021-02-19 19:45:33 +00:00
commit ce99c2a349
120 changed files with 3474 additions and 1450 deletions

View file

@ -41,6 +41,7 @@ IObject
IPackage
IPeasant
IStorage
IStringable
ITab
ITaskbar
LCID
@ -93,6 +94,8 @@ TBPF
THEMECHANGED
tmp
tolower
TTask
TVal
tx
UPDATEINIFILE
userenv

View file

@ -521,6 +521,8 @@ DECAUPSS
DECAWM
DECCKM
DECCOLM
DECDHL
DECDWL
DECEKBD
DECID
DECKPAM
@ -557,6 +559,7 @@ DECSR
decstandar
DECSTBM
DECSTR
DECSWL
DECTCEM
Dedupe
deduplicated
@ -2825,6 +2828,7 @@ Xes
XES
xff
XFile
XFORM
XManifest
XMath
XMFLOAT
@ -2843,6 +2847,8 @@ XSubstantial
xtended
xterm
XTest
XTPUSHSGR
XTPOPSGR
xunit
xutr
xvalue

View file

@ -76,6 +76,7 @@
"copy",
"duplicateTab",
"find",
"findMatch",
"moveFocus",
"moveTab",
"newTab",
@ -138,6 +139,13 @@
],
"type": "string"
},
"FindMatchDirection": {
"enum": [
"next",
"prev"
],
"type": "string"
},
"SplitState": {
"enum": [
"vertical",
@ -559,6 +567,23 @@
}
]
},
"FindMatchAction": {
"description": "Arguments corresponding to a Find Match Action",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "findMatch" },
"direction": {
"$ref": "#/definitions/FindMatchDirection",
"default": "prev",
"description": "The direction to search in. \"prev\" will search upwards in the buffer, and \"next\" will search downwards."
}
}
}
],
"required": [ "direction" ]
},
"Keybinding": {
"additionalProperties": false,
"properties": {
@ -583,6 +608,7 @@
{ "$ref": "#/definitions/ScrollUpAction" },
{ "$ref": "#/definitions/ScrollDownAction" },
{ "$ref": "#/definitions/MoveTabAction" },
{ "$ref": "#/definitions/FindMatchAction" },
{ "type": "null" }
]
},

View file

@ -0,0 +1,36 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- LineRendition.hpp
Abstract:
- Enumerated type for the VT line rendition attribute. This determines the
width and height scaling with which each line is rendered.
--*/
#pragma once
enum class LineRendition
{
SingleWidth,
DoubleWidth,
DoubleHeightTop,
DoubleHeightBottom
};
constexpr SMALL_RECT ScreenToBufferLine(const SMALL_RECT& line, const LineRendition lineRendition)
{
// Use shift right to quickly divide the Left and Right by 2 for double width lines.
const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1;
return { line.Left >> scale, line.Top, line.Right >> scale, line.Bottom };
}
constexpr SMALL_RECT BufferToScreenLine(const SMALL_RECT& line, const LineRendition lineRendition)
{
// Use shift left to quickly multiply the Left and Right by 2 for double width lines.
const SHORT scale = lineRendition == LineRendition::SingleWidth ? 0 : 1;
return { line.Left << scale, line.Top, (line.Right << scale) + scale, line.Bottom };
}

View file

@ -21,6 +21,7 @@ ROW::ROW(const SHORT rowId, const unsigned short rowWidth, const TextAttribute f
_rowWidth{ rowWidth },
_charRow{ rowWidth, this },
_attrRow{ rowWidth, fillAttribute },
_lineRendition{ LineRendition::SingleWidth },
_wrapForced{ false },
_doubleBytePadded{ false },
_pParent{ pParent }
@ -35,6 +36,7 @@ ROW::ROW(const SHORT rowId, const unsigned short rowWidth, const TextAttribute f
// - <none>
bool ROW::Reset(const TextAttribute Attr)
{
_lineRendition = LineRendition::SingleWidth;
_wrapForced = false;
_doubleBytePadded = false;
_charRow.Reset();

View file

@ -21,6 +21,7 @@ Revision History:
#pragma once
#include "AttrRow.hpp"
#include "LineRendition.hpp"
#include "OutputCell.hpp"
#include "OutputCellIterator.hpp"
#include "CharRow.hpp"
@ -48,6 +49,9 @@ public:
const ATTR_ROW& GetAttrRow() const noexcept { return _attrRow; }
ATTR_ROW& GetAttrRow() noexcept { return _attrRow; }
LineRendition GetLineRendition() const noexcept { return _lineRendition; }
void SetLineRendition(const LineRendition lineRendition) noexcept { _lineRendition = lineRendition; }
SHORT GetId() const noexcept { return _id; }
void SetId(const SHORT id) noexcept { _id = id; }
@ -70,6 +74,7 @@ public:
private:
CharRow _charRow;
ATTR_ROW _attrRow;
LineRendition _lineRendition;
SHORT _id;
unsigned short _rowWidth;
// Occurs when the user runs out of text in a given row and we're forced to wrap the cursor to the next line

View file

@ -38,6 +38,7 @@
<ClInclude Include="..\cursor.h" />
<ClInclude Include="..\DbcsAttribute.hpp" />
<ClInclude Include="..\ICharRow.hpp" />
<ClInclude Include="..\LineRendition.hpp" />
<ClInclude Include="..\OutputCell.hpp" />
<ClInclude Include="..\OutputCellIterator.hpp" />
<ClInclude Include="..\OutputCellRect.hpp" />

View file

@ -97,7 +97,12 @@ bool Search::FindNext()
// - Takes the found word and selects it in the screen buffer
void Search::Select() const
{
_uiaData.SelectNewRegion(_coordSelStart, _coordSelEnd);
// Convert buffer selection offsets into the equivalent screen coordinates
// required by SelectNewRegion, taking line renditions into account.
const auto& textBuffer = _uiaData.GetTextBuffer();
const auto selStart = textBuffer.BufferToScreenPosition(_coordSelStart);
const auto selEnd = textBuffer.BufferToScreenPosition(_coordSelEnd);
_uiaData.SelectNewRegion(selStart, selEnd);
}
// Routine Description:
@ -141,7 +146,10 @@ COORD Search::s_GetInitialAnchor(IUiaData& uiaData, const Direction direction)
const COORD textBufferEndPosition = uiaData.GetTextBufferEndPosition();
if (uiaData.IsSelectionActive())
{
auto anchor = uiaData.GetSelectionAnchor();
// Convert the screen position of the selection anchor into an equivalent
// buffer position to start searching, taking line rendition into account.
auto anchor = textBuffer.ScreenToBufferPosition(uiaData.GetSelectionAnchor());
if (direction == Direction::Forward)
{
textBuffer.GetSize().IncrementInBoundsCircular(anchor);

View file

@ -290,13 +290,14 @@ bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute
// We only need to compensate for leading bytes
if (dbcsAttribute.IsLeading())
{
short const sBufferWidth = GetSize().Width();
const auto cursorPosition = GetCursor().GetPosition();
const auto lineWidth = GetLineWidth(cursorPosition.Y);
// If we're about to lead on the last column in the row, we need to add a padding space
if (GetCursor().GetPosition().X == sBufferWidth - 1)
if (cursorPosition.X == lineWidth - 1)
{
// set that we're wrapping for double byte reasons
auto& row = GetRowByOffset(GetCursor().GetPosition().Y);
auto& row = GetRowByOffset(cursorPosition.Y);
row.SetDoubleBytePadded(true);
// then move the cursor forward and onto the next row
@ -496,7 +497,7 @@ bool TextBuffer::IncrementCursor()
// Cursor position is stored as logical array indices (starts at 0) for the window
// Buffer Size is specified as the "length" of the array. It would say 80 for valid values of 0-79.
// So subtract 1 from buffer size in each direction to find the index of the final column in the buffer
const short iFinalColumnIndex = GetSize().RightInclusive();
const short iFinalColumnIndex = GetLineWidth(GetCursor().GetPosition().Y) - 1;
// Move the cursor one position to the right
GetCursor().IncrementXPosition(1);
@ -635,7 +636,7 @@ COORD TextBuffer::GetLastNonSpaceCharacter(std::optional<const Microsoft::Consol
// Return Value:
// - Coordinate position in screen coordinates of the character just before the cursor.
// - NOTE: Will return 0,0 if already in the top left corner
COORD TextBuffer::_GetPreviousFromCursor() const noexcept
COORD TextBuffer::_GetPreviousFromCursor() const
{
COORD coordPosition = GetCursor().GetPosition();
@ -649,11 +650,11 @@ COORD TextBuffer::_GetPreviousFromCursor() const noexcept
// Otherwise, only if we're not on the top row (e.g. we don't move anywhere in the top left corner. there is no previous)
if (coordPosition.Y > 0)
{
// move the cursor to the right edge
coordPosition.X = GetSize().RightInclusive();
// and up one line
// move the cursor up one line
coordPosition.Y--;
// and to the right edge
coordPosition.X = GetLineWidth(coordPosition.Y) - 1;
}
}
@ -801,6 +802,78 @@ void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) no
_currentAttributes = currentAttributes;
}
void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition)
{
const auto cursorPosition = GetCursor().GetPosition();
const auto rowIndex = cursorPosition.Y;
auto& row = GetRowByOffset(rowIndex);
if (row.GetLineRendition() != lineRendition)
{
row.SetLineRendition(lineRendition);
// If the line rendition has changed, the row can no longer be wrapped.
row.SetWrapForced(false);
// And if it's no longer single width, the right half of the row should be erased.
if (lineRendition != LineRendition::SingleWidth)
{
const auto fillChar = L' ';
auto fillAttrs = GetCurrentAttributes();
fillAttrs.SetStandardErase();
const size_t fillOffset = GetLineWidth(rowIndex);
const size_t fillLength = GetSize().Width() - fillOffset;
const auto fillData = OutputCellIterator{ fillChar, fillAttrs, fillLength };
row.WriteCells(fillData, fillOffset, false);
// We also need to make sure the cursor is clamped within the new width.
GetCursor().SetPosition(ClampPositionWithinLine(cursorPosition));
}
_NotifyPaint(Viewport::FromDimensions({ 0, rowIndex }, { GetSize().Width(), 1 }));
}
}
void TextBuffer::ResetLineRenditionRange(const size_t startRow, const size_t endRow)
{
for (auto row = startRow; row < endRow; row++)
{
GetRowByOffset(row).SetLineRendition(LineRendition::SingleWidth);
}
}
LineRendition TextBuffer::GetLineRendition(const size_t row) const
{
return GetRowByOffset(row).GetLineRendition();
}
bool TextBuffer::IsDoubleWidthLine(const size_t row) const
{
return GetLineRendition(row) != LineRendition::SingleWidth;
}
SHORT TextBuffer::GetLineWidth(const size_t row) const
{
// Use shift right to quickly divide the width by 2 for double width lines.
const auto scale = IsDoubleWidthLine(row) ? 1 : 0;
return GetSize().Width() >> scale;
}
COORD TextBuffer::ClampPositionWithinLine(const COORD position) const
{
const SHORT rightmostColumn = GetLineWidth(position.Y) - 1;
return { std::min(position.X, rightmostColumn), position.Y };
}
COORD TextBuffer::ScreenToBufferPosition(const COORD position) const
{
// Use shift right to quickly divide the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.Y) ? 1 : 0;
return { position.X >> scale, position.Y };
}
COORD TextBuffer::BufferToScreenPosition(const COORD position) const
{
// Use shift left to quickly multiply the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.Y) ? 1 : 0;
return { position.X << scale, position.Y };
}
// Routine Description:
// - Resets the text contents of this buffer with the default character
// and the default current color attributes
@ -1425,9 +1498,11 @@ bool TextBuffer::MoveToPreviousGlyph(til::point& pos) const
// - blockSelection: when enabled, only get the rectangular text region,
// as opposed to the text extending to the left/right
// buffer margins
// - bufferCoordinates: when enabled, treat the coordinates as relative to
// the buffer rather than the screen.
// Return Value:
// - the delimiter class for the given char
const std::vector<SMALL_RECT> TextBuffer::GetTextRects(COORD start, COORD end, bool blockSelection) const
const std::vector<SMALL_RECT> TextBuffer::GetTextRects(COORD start, COORD end, bool blockSelection, bool bufferCoordinates) const
{
std::vector<SMALL_RECT> textRects;
@ -1461,6 +1536,13 @@ const std::vector<SMALL_RECT> TextBuffer::GetTextRects(COORD start, COORD end, b
textRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : bufferSize.RightInclusive();
}
// If we were passed screen coordinates, convert the given range into
// equivalent buffer offsets, taking line rendition into account.
if (!bufferCoordinates)
{
textRow = ScreenToBufferLine(textRow, GetLineRendition(row));
}
_ExpandTextRow(textRow);
textRects.emplace_back(textRow);
}
@ -2044,7 +2126,6 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport);
const short cOldRowsTotal = cOldLastChar.Y + 1;
const short cOldColsTotal = oldBuffer.GetSize().Width();
COORD cNewCursorPos = { 0 };
bool fFoundCursorPos = false;
@ -2056,9 +2137,19 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
{
// Fetch the row and its "right" which is the last printable character.
const ROW& row = oldBuffer.GetRowByOffset(iOldRow);
const short cOldColsTotal = oldBuffer.GetLineWidth(iOldRow);
const CharRow& charRow = row.GetCharRow();
short iRight = gsl::narrow_cast<short>(charRow.MeasureRight());
// If we're starting a new row, try and preserve the line rendition
// from the row in the original buffer.
const auto newBufferPos = newBuffer.GetCursor().GetPosition();
if (newBufferPos.X == 0)
{
auto& newRow = newBuffer.GetRowByOffset(newBufferPos.Y);
newRow.SetLineRendition(row.GetLineRendition());
}
// There is a special case here. If the row has a "wrap"
// flag on it, but the right isn't equal to the width (one
// index past the final valid index in the row) then there

View file

@ -122,6 +122,16 @@ public:
void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept;
void SetCurrentLineRendition(const LineRendition lineRendition);
void ResetLineRenditionRange(const size_t startRow, const size_t endRow);
LineRendition GetLineRendition(const size_t row) const;
bool IsDoubleWidthLine(const size_t row) const;
SHORT GetLineWidth(const size_t row) const;
COORD ClampPositionWithinLine(const COORD position) const;
COORD ScreenToBufferPosition(const COORD position) const;
COORD BufferToScreenPosition(const COORD position) const;
void Reset();
[[nodiscard]] HRESULT ResizeTraditional(const COORD newSize) noexcept;
@ -141,7 +151,7 @@ public:
bool MoveToNextGlyph(til::point& pos, bool allowBottomExclusive = false) const;
bool MoveToPreviousGlyph(til::point& pos) const;
const std::vector<SMALL_RECT> GetTextRects(COORD start, COORD end, bool blockSelection = false) const;
const std::vector<SMALL_RECT> GetTextRects(COORD start, COORD end, bool blockSelection, bool bufferCoordinates) const;
void AddHyperlinkToMap(std::wstring_view uri, uint16_t id);
std::wstring GetHyperlinkUriFromId(uint16_t id) const;
@ -212,7 +222,7 @@ private:
void _SetFirstRowIndex(const SHORT FirstRowIndex) noexcept;
COORD _GetPreviousFromCursor() const noexcept;
COORD _GetPreviousFromCursor() const;
void _SetWrapOnCurrentRow();
void _AdjustWrapOnCurrentRow(const bool fSet);

View file

@ -12,7 +12,7 @@
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
IgnorableNamespaces="uap mp rescap">
IgnorableNamespaces="uap mp rescap uap3">
<Identity
Name="WindowsTerminalDev"
@ -69,6 +69,11 @@
Enabled="false"
DisplayName="ms-resource:AppNameDev" />
</uap5:Extension>
<uap3:Extension Category="windows.appExtensionHost">
<uap3:AppExtensionHost>
<uap3:Name>com.microsoft.windows.terminal.settings</uap3:Name>
</uap3:AppExtensionHost>
</uap3:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>

View file

@ -13,7 +13,7 @@
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap mp rescap">
IgnorableNamespaces="uap mp rescap uap3">
<Identity
Name="Microsoft.WindowsTerminalPreview"
@ -64,6 +64,11 @@
<desktop:ExecutionAlias Alias="wt.exe" />
</uap3:AppExecutionAlias>
</uap3:Extension>
<uap3:Extension Category="windows.appExtensionHost">
<uap3:AppExtensionHost>
<uap3:Name>com.microsoft.windows.terminal.settings</uap3:Name>
</uap3:AppExtensionHost>
</uap3:Extension>
<uap5:Extension Category="windows.startupTask">
<uap5:StartupTask
TaskId="StartTerminalOnLoginTask"

View file

@ -13,7 +13,7 @@
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap mp rescap">
IgnorableNamespaces="uap mp rescap uap3">
<Identity
Name="Microsoft.WindowsTerminal"
@ -64,6 +64,11 @@
<desktop:ExecutionAlias Alias="wt.exe" />
</uap3:AppExecutionAlias>
</uap3:Extension>
<uap3:Extension Category="windows.appExtensionHost">
<uap3:AppExtensionHost>
<uap3:Name>com.microsoft.windows.terminal.settings</uap3:Name>
</uap3:AppExtensionHost>
</uap3:Extension>
<uap5:Extension Category="windows.startupTask">
<uap5:StartupTask
TaskId="StartTerminalOnLoginTask"

View file

@ -275,14 +275,6 @@ namespace SettingsModelLocalTests
void CommandTests::TestAutogeneratedName()
{
// Tests run in Helix can't report Skipped until GH#7286 is resolved.
// Set ignore flag to make Helix run completely overlook it.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True")
END_TEST_METHOD_PROPERTIES()
// This test to be corrected as a part of GH#7281
// This test ensures that we'll correctly create commands for actions
// that don't have given names, pursuant to the spec in GH#6532.

View file

@ -34,6 +34,7 @@ Author(s):
#include <WexTestClass.h>
#include <json.h>
#include "consoletaeftemplates.hpp"
#include "winrtTaefTemplates.hpp"
#include <winrt/Windows.ApplicationModel.Resources.Core.h>
#include "winrt/Windows.UI.Xaml.Markup.h"

View file

@ -4,6 +4,7 @@
#include "pch.h"
#include "../TerminalApp/CommandLinePaletteItem.h"
#include "../TerminalApp/CommandPalette.h"
#include "../CppWinrtTailored.h"
using namespace Microsoft::Console;
using namespace WEX::Logging;
@ -29,187 +30,203 @@ namespace TerminalAppLocalTests
void FilteredCommandTests::VerifyHighlighting()
{
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
{
Log::Comment(L"Testing command name segmentation with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter equals to the string");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter with first character matching");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"A";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 2u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter with other case");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"a";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 2u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter matching several characters");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"ab";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 4u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAA");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(2).TextSegment(), L"B");
VERIFY_IS_TRUE(segments.GetAt(2).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(3).TextSegment(), L"BBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(3).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with non matching filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"abcd";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
auto result = RunOnUIThread([]() {
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
{
Log::Comment(L"Testing command name segmentation with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter equals to the string");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter with first character matching");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"A";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 2u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter with other case");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"a";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 2u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter matching several characters");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"ab";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 4u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAA");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(2).TextSegment(), L"B");
VERIFY_IS_TRUE(segments.GetAt(2).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(3).TextSegment(), L"BBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(3).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with non matching filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"abcd";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
});
VERIFY_SUCCEEDED(result);
}
void FilteredCommandTests::VerifyWeight()
{
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
{
Log::Comment(L"Testing weight of command with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 0);
}
{
Log::Comment(L"Testing weight of command with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 0);
}
{
Log::Comment(L"Testing weight of command with filter equals to the string");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 30); // 1 point for the first char and 2 points for the 14 consequent ones + 1 point for the beginning of the word
}
{
Log::Comment(L"Testing weight of command with filter with first character matching");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"A";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
}
{
Log::Comment(L"Testing weight of command with filter with other case");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"a";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
}
{
Log::Comment(L"Testing weight of command with filter matching several characters");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"ab";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 3); // 1 point for the first char match + 1 point for the beginning of the word + 1 point for the match of "b"
}
auto result = RunOnUIThread([]() {
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
{
Log::Comment(L"Testing weight of command with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 0);
}
{
Log::Comment(L"Testing weight of command with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 0);
}
{
Log::Comment(L"Testing weight of command with filter equals to the string");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 30); // 1 point for the first char and 2 points for the 14 consequent ones + 1 point for the beginning of the word
}
{
Log::Comment(L"Testing weight of command with filter with first character matching");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"A";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
}
{
Log::Comment(L"Testing weight of command with filter with other case");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"a";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
}
{
Log::Comment(L"Testing weight of command with filter matching several characters");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"ab";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 3); // 1 point for the first char match + 1 point for the beginning of the word + 1 point for the match of "b"
}
});
VERIFY_SUCCEEDED(result);
}
void FilteredCommandTests::VerifyCompare()
{
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
const auto paletteItem2{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"BBBBBCCC") };
{
Log::Comment(L"Testing comparison of commands with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
auto result = RunOnUIThread([]() {
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
const auto paletteItem2{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"BBBBBCCC") };
{
Log::Comment(L"Testing comparison of commands with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
{
Log::Comment(L"Testing comparison of commands with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
filteredCommand->_Weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
{
Log::Comment(L"Testing comparison of commands with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
filteredCommand->_Weight = filteredCommand->_computeWeight();
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
filteredCommand2->_Filter = L"";
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
filteredCommand2->_Filter = L"";
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
{
Log::Comment(L"Testing comparison of commands with different weights");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"B";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
filteredCommand->_Weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
{
Log::Comment(L"Testing comparison of commands with different weights");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
filteredCommand->_Filter = L"B";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
filteredCommand->_Weight = filteredCommand->_computeWeight();
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
filteredCommand2->_Filter = L"B";
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
filteredCommand2->_Filter = L"B";
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
VERIFY_IS_TRUE(filteredCommand->Weight() < filteredCommand2->Weight()); // Second command gets more points due to the beginning of the word
VERIFY_IS_FALSE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
VERIFY_IS_TRUE(filteredCommand->Weight() < filteredCommand2->Weight()); // Second command gets more points due to the beginning of the word
VERIFY_IS_FALSE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
});
VERIFY_SUCCEEDED(result);
}
void FilteredCommandTests::VerifyCompareIgnoreCase()
{
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"a") };
const auto paletteItem2{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"B") };
{
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
auto result = RunOnUIThread([]() {
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"a") };
const auto paletteItem2{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"B") };
{
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
});
VERIFY_SUCCEEDED(result);
}
}

View file

@ -868,13 +868,33 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(4u, page->_mruTabs.Size());
Log::Comment(L"give alphabetical names to all switch tab actions");
RunOnUIThread([&page]() {
TestOnUIThread([&page]() {
page->_GetTerminalTabImpl(page->_tabs.GetAt(0))->Title(L"a");
});
TestOnUIThread([&page]() {
page->_GetTerminalTabImpl(page->_tabs.GetAt(1))->Title(L"b");
});
TestOnUIThread([&page]() {
page->_GetTerminalTabImpl(page->_tabs.GetAt(2))->Title(L"c");
});
TestOnUIThread([&page]() {
page->_GetTerminalTabImpl(page->_tabs.GetAt(3))->Title(L"d");
});
TestOnUIThread([&page]() {
Log::Comment(L"Sanity check the titles of our tabs are what we set them to.");
VERIFY_ARE_EQUAL(L"a", page->_tabs.GetAt(0).Title());
VERIFY_ARE_EQUAL(L"b", page->_tabs.GetAt(1).Title());
VERIFY_ARE_EQUAL(L"c", page->_tabs.GetAt(2).Title());
VERIFY_ARE_EQUAL(L"d", page->_tabs.GetAt(3).Title());
VERIFY_ARE_EQUAL(L"d", page->_mruTabs.GetAt(0).Title());
VERIFY_ARE_EQUAL(L"c", page->_mruTabs.GetAt(1).Title());
VERIFY_ARE_EQUAL(L"b", page->_mruTabs.GetAt(2).Title());
VERIFY_ARE_EQUAL(L"a", page->_mruTabs.GetAt(3).Title());
});
Log::Comment(L"Change the tab switch order to MRU switching");
TestOnUIThread([&page]() {
page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::MostRecentlyUsed);
@ -897,18 +917,30 @@ namespace TerminalAppLocalTests
Log::Comment(L"Switch to the next MRU tab, which is the third tab");
RunOnUIThread([&page]() {
page->_SelectNextTab(true);
// In the course of a single tick, the Command Palette will:
// * open
// * select the proper tab from the mru's list
// * raise an event for _filteredActionsView().SelectionChanged to
// immediately preview the new tab
// * raise a _SwitchToTabRequestedHandlers event
// * then dismiss itself, because we can't fake holing down an
// anchor key in the tests
});
TestOnUIThread([&page]() {
VERIFY_ARE_EQUAL(L"c", page->_mruTabs.GetAt(0).Title());
VERIFY_ARE_EQUAL(L"d", page->_mruTabs.GetAt(1).Title());
VERIFY_ARE_EQUAL(L"b", page->_mruTabs.GetAt(2).Title());
VERIFY_ARE_EQUAL(L"a", page->_mruTabs.GetAt(3).Title());
});
const auto palette = winrt::get_self<winrt::TerminalApp::implementation::CommandPalette>(page->CommandPalette());
VERIFY_ARE_EQUAL(1u, palette->_switcherStartIdx, L"Verify the index is 1 as we went right");
VERIFY_ARE_EQUAL(winrt::TerminalApp::implementation::CommandPaletteMode::TabSwitchMode, palette->_currentMode, L"Verify we are in the tab switcher mode");
Log::Comment(L"Verify command palette preserves MRU order of tabs");
VERIFY_ARE_EQUAL(4u, palette->_filteredActions.Size());
VERIFY_ARE_EQUAL(L"d", palette->_filteredActions.GetAt(0).Item().Name());
VERIFY_ARE_EQUAL(L"c", palette->_filteredActions.GetAt(1).Item().Name());
VERIFY_ARE_EQUAL(L"b", palette->_filteredActions.GetAt(2).Item().Name());
VERIFY_ARE_EQUAL(L"a", palette->_filteredActions.GetAt(3).Item().Name());
// At this point, the contents of the command palette's _mruTabs list is
// still the _old_ ordering (d, c, b, a). The ordering is only updated
// in TerminalPage::_SelectNextTab, but as we saw before, the palette
// will also dismiss itself immediately when that's called. So we can't
// really inspect the contents of the list in this test, unfortunately.
}
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" IgnorableNamespaces="uap mp">
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" IgnorableNamespaces="uap mp uap3">
<Identity Name="WindowsTerminal.TestHost" Publisher="CN=Windows Terminal Team" Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="fba054a7-f1a1-4cb7-bb21-4949919af2f5" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
<Properties>
@ -22,6 +22,13 @@
</uap:DefaultTile>
<uap:SplashScreen Image="taef.png" />
</uap:VisualElements>
<Extensions>
<uap3:Extension Category="windows.appExtensionHost">
<uap3:AppExtensionHost>
<uap3:Name>com.microsoft.windows.terminal.settings</uap3:Name>
</uap3:AppExtensionHost>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
</Package>

View file

@ -34,6 +34,7 @@ Author(s):
#include <WexTestClass.h>
#include <json.h>
#include "consoletaeftemplates.hpp"
#include "winrtTaefTemplates.hpp"
#include <winrt/Windows.ApplicationModel.Resources.Core.h>
#include "winrt/Windows.UI.Xaml.Markup.h"

View file

@ -193,6 +193,18 @@ namespace winrt::TerminalApp::implementation
args.Handled(true);
}
void TerminalPage::_HandleFindMatch(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (const auto& realArgs = args.ActionArgs().try_as<FindMatchArgs>())
{
if (const auto& control{ _GetActiveControl() })
{
control.SearchMatch(realArgs.Direction() == FindMatchDirection::Next);
args.Handled(true);
}
}
}
void TerminalPage::_HandleOpenSettings(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{

View file

@ -172,6 +172,9 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - Returns the settings currently in use by the entire Terminal application.
// - IMPORTANT! This can throw! Make sure to try/catch this, so that the
// LocalTests don't crash (because their Application::Current() won't be a
// AppLogic)
// Throws:
// - HR E_INVALIDARG if the app isn't up and running.
const CascadiaSettings AppLogic::CurrentAppSettings()

View file

@ -256,6 +256,11 @@ namespace winrt::TerminalApp::implementation
_BreakIntoDebuggerHandlers(*this, eventArgs);
break;
}
case ShortcutAction::FindMatch:
{
_FindMatchHandlers(*this, eventArgs);
break;
}
case ShortcutAction::TogglePaneReadOnly:
{
_TogglePaneReadOnlyHandlers(*this, eventArgs);

View file

@ -65,6 +65,7 @@ namespace winrt::TerminalApp::implementation
TYPED_EVENT(TabSearch, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs);
TYPED_EVENT(MoveTab, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs);
TYPED_EVENT(BreakIntoDebugger, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs);
TYPED_EVENT(FindMatch, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs);
TYPED_EVENT(TogglePaneReadOnly, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs);
// clang-format on

View file

@ -51,6 +51,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, Microsoft.Terminal.Settings.Model.ActionEventArgs> TabSearch;
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, Microsoft.Terminal.Settings.Model.ActionEventArgs> MoveTab;
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, Microsoft.Terminal.Settings.Model.ActionEventArgs> BreakIntoDebugger;
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, Microsoft.Terminal.Settings.Model.ActionEventArgs> FindMatch;
event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, Microsoft.Terminal.Settings.Model.ActionEventArgs> TogglePaneReadOnly;
}
}

View file

@ -135,8 +135,7 @@ namespace winrt::TerminalApp::implementation
}
CATCH_LOG();
_tabRow.PointerMoved({ this, &TerminalPage::_RestorePointerCursorHandler });
_tabRow.PointerMoved({ get_weak(), &TerminalPage::_RestorePointerCursorHandler });
_tabView.CanReorderTabs(!isElevated);
_tabView.CanDragTabs(!isElevated);
@ -261,7 +260,11 @@ namespace winrt::TerminalApp::implementation
// Store cursor, so we can restore it, e.g., after mouse vanishing
// (we'll need to adapt this logic once we make cursor context aware)
_defaultPointerCursor = CoreWindow::GetForCurrentThread().PointerCursor();
try
{
_defaultPointerCursor = CoreWindow::GetForCurrentThread().PointerCursor();
}
CATCH_LOG();
}
// Method Description:
@ -1128,6 +1131,7 @@ namespace winrt::TerminalApp::implementation
_actionDispatch->TabSearch({ this, &TerminalPage::_HandleOpenTabSearch });
_actionDispatch->MoveTab({ this, &TerminalPage::_HandleMoveTab });
_actionDispatch->BreakIntoDebugger({ this, &TerminalPage::_HandleBreakIntoDebugger });
_actionDispatch->FindMatch({ this, &TerminalPage::_HandleFindMatch });
_actionDispatch->TogglePaneReadOnly({ this, &TerminalPage::_HandleTogglePaneReadOnly });
}
@ -1386,8 +1390,8 @@ namespace winrt::TerminalApp::implementation
// Add an event handler for when the terminal wants to set a progress indicator on the taskbar
term.SetTaskbarProgress({ this, &TerminalPage::_SetTaskbarProgressHandler });
term.HidePointerCursor({ this, &TerminalPage::_HidePointerCursorHandler });
term.RestorePointerCursor({ this, &TerminalPage::_RestorePointerCursorHandler });
term.HidePointerCursor({ get_weak(), &TerminalPage::_HidePointerCursorHandler });
term.RestorePointerCursor({ get_weak(), &TerminalPage::_RestorePointerCursorHandler });
// Bind Tab events to the TermControl and the Tab's Pane
hostingTab.Initialize(term);
@ -3195,8 +3199,15 @@ namespace winrt::TerminalApp::implementation
{
if (_shouldMouseVanish && !_isMouseHidden)
{
CoreWindow::GetForCurrentThread().PointerCursor(nullptr);
_isMouseHidden = true;
if (auto window{ CoreWindow::GetForCurrentThread() })
{
try
{
window.PointerCursor(nullptr);
_isMouseHidden = true;
}
CATCH_LOG();
}
}
}
@ -3208,8 +3219,15 @@ namespace winrt::TerminalApp::implementation
{
if (_isMouseHidden)
{
CoreWindow::GetForCurrentThread().PointerCursor(_defaultPointerCursor);
_isMouseHidden = false;
if (auto window{ CoreWindow::GetForCurrentThread() })
{
try
{
window.PointerCursor(_defaultPointerCursor);
_isMouseHidden = false;
}
CATCH_LOG();
}
}
}

View file

@ -315,6 +315,7 @@ namespace winrt::TerminalApp::implementation
void _HandleOpenTabSearch(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
void _HandleMoveTab(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
void _HandleBreakIntoDebugger(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
void _HandleFindMatch(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
void _HandleTogglePaneReadOnly(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
// Make sure to hook new actions up in _RegisterActionCallbacks!

View file

@ -104,15 +104,21 @@ namespace winrt::TerminalApp::implementation
if (auto tab{ weakThis.get() })
{
const auto settings{ winrt::TerminalApp::implementation::AppLogic::CurrentAppSettings() };
if (settings.GlobalSettings().TabWidthMode() == winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::SizeToContent)
try
{
tab->_headerControl.RenamerMaxWidth(HeaderRenameBoxWidthTitleLength);
}
else
{
tab->_headerControl.RenamerMaxWidth(HeaderRenameBoxWidthDefault);
// 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);
}
}
CATCH_LOG()
}
}
@ -609,6 +615,19 @@ namespace winrt::TerminalApp::implementation
tab->_RecalculateAndApplyReadOnly();
}
});
control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) {
if (const auto tab{ weakThis.get() })
{
if (tab->_focusState != FocusState::Unfocused)
{
if (const auto termControl{ sender.try_as<winrt::Microsoft::Terminal::TerminalControl::TermControl>() })
{
termControl.Focus(FocusState::Pointer);
}
}
}
});
}
// Method Description:
@ -722,25 +741,26 @@ namespace winrt::TerminalApp::implementation
}
});
// Add a PaneRaiseVisualBell event handler to the Pane. When the pane emits this event,
// we need to bubble it all the way to app host. In this part of the chain we bubble it
// from the hosting tab to the page.
// Add a PaneRaiseBell event handler to the Pane
pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) {
if (auto tab{ weakThis.get() })
{
if (visual)
{
// If visual is set, we need to bubble this event all the way to app host to flash the taskbar
// In this part of the chain we bubble it from the hosting tab to the page
tab->_TabRaiseVisualBellHandlers();
}
tab->ShowBellIndicator(true);
// Show the bell indicator in the tab header
tab->ShowBellIndicator(true);
// If this tab is focused, activate the bell indicator timer, which will
// remove the bell indicator once it fires
// (otherwise, the indicator is removed when the tab gets focus)
if (tab->_focusState != WUX::FocusState::Unfocused)
{
tab->ActivateBellIndicatorTimer();
}
// If this tab is focused, activate the bell indicator timer, which will
// remove the bell indicator once it fires
// (otherwise, the indicator is removed when the tab gets focus)
if (tab->_focusState != WUX::FocusState::Unfocused)
{
tab->ActivateBellIndicatorTimer();
}
}
});

View file

@ -225,6 +225,18 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}
}
void TermControl::SearchMatch(const bool goForward)
{
if (!_searchBox)
{
CreateSearchBoxControl();
}
else
{
_Search(_searchBox->TextBox().Text(), goForward, false);
}
}
// Method Description:
// - Search text in text buffer. This is triggered if the user click
// search button or press enter.
@ -234,7 +246,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - caseSensitive: boolean that represents if the current search is case sensitive
// Return Value:
// - <none>
void TermControl::_Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive)
void TermControl::_Search(const winrt::hstring& text,
const bool goForward,
const bool caseSensitive)
{
if (text.size() == 0 || _closing)
{
@ -267,7 +281,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - RoutedEventArgs: not used
// Return Value:
// - <none>
void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/, RoutedEventArgs const& /*args*/)
void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/,
RoutedEventArgs const& /*args*/)
{
_searchBox->Visibility(Visibility::Collapsed);
@ -1098,7 +1113,13 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// modifier key. We'll wait for a real keystroke to dismiss the
// GH #7395 - don't dismiss selection when taking PrintScreen
// selection.
if (_terminal->IsSelectionActive() && !KeyEvent::IsModifierKey(vkey) && vkey != VK_SNAPSHOT)
// GH#8522, GH#3758 - Only dismiss the selection on key _down_. If we
// dismiss on key up, then there's chance that we'll immediately dismiss
// a selection created by an action bound to a keydown.
if (_terminal->IsSelectionActive() &&
!KeyEvent::IsModifierKey(vkey) &&
vkey != VK_SNAPSHOT &&
keyDown)
{
const CoreWindow window = CoreWindow::GetForCurrentThread();
const auto leftWinKeyState = window.GetKeyState(VirtualKey::LeftWindows);
@ -1376,7 +1397,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
if (!_focused && _settings.FocusFollowMouse())
{
Focus(FocusState::Pointer);
_FocusFollowMouseRequestedHandlers(*this, nullptr);
}
if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen)

View file

@ -133,6 +133,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void CreateSearchBoxControl();
void SearchMatch(const bool goForward);
bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down);
bool OnMouseWheel(const Windows::Foundation::Point location, const int32_t delta, const bool leftButtonDown, const bool midButtonDown, const bool rightButtonDown);
@ -186,6 +188,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
TYPED_EVENT(HidePointerCursor, IInspectable, IInspectable);
TYPED_EVENT(RestorePointerCursor, IInspectable, IInspectable);
TYPED_EVENT(ReadOnlyChanged, IInspectable, IInspectable);
TYPED_EVENT(FocusFollowMouseRequested, IInspectable, IInspectable);
// clang-format on
private:
@ -342,7 +345,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _CompositionCompleted(winrt::hstring text);
void _CurrentCursorPositionHandler(const IInspectable& sender, const CursorPositionEventArgs& eventArgs);
void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs);
winrt::fire_and_forget _AsyncCloseConnection();
winrt::fire_and_forget _RaiseReadOnlyWarning();

View file

@ -102,6 +102,8 @@ namespace Microsoft.Terminal.TerminalControl
void CreateSearchBoxControl();
void SearchMatch(Boolean goForward);
void AdjustFontSize(Int32 fontSizeDelta);
void ResetFontSize();
@ -120,5 +122,6 @@ namespace Microsoft.Terminal.TerminalControl
Boolean ReadOnly { get; };
void ToggleReadOnly();
event Windows.Foundation.TypedEventHandler<Object, Object> ReadOnlyChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> FocusFollowMouseRequested;
}
}

View file

@ -70,6 +70,9 @@ namespace Microsoft::Terminal::Core
virtual bool SetWorkingDirectory(std::wstring_view uri) noexcept = 0;
virtual std::wstring_view GetWorkingDirectory() noexcept = 0;
virtual bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept = 0;
virtual bool PopGraphicsRendition() noexcept = 0;
protected:
ITerminalApi() = default;
};

View file

@ -6,6 +6,7 @@
#include <conattrs.hpp>
#include "../../buffer/out/textBuffer.hpp"
#include "../../types/inc/sgrStack.hpp"
#include "../../renderer/inc/BlinkingState.hpp"
#include "../../terminal/parser/StateMachine.hpp"
#include "../../terminal/input/terminalInput.hpp"
@ -124,6 +125,10 @@ public:
bool SetTaskbarProgress(const size_t state, const size_t progress) noexcept override;
bool SetWorkingDirectory(std::wstring_view uri) noexcept override;
std::wstring_view GetWorkingDirectory() noexcept override;
bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override;
bool PopGraphicsRendition() noexcept override;
#pragma endregion
#pragma region ITerminalInput
@ -347,6 +352,8 @@ private:
COORD _ConvertToBufferCell(const COORD viewportPos) const;
#pragma endregion
Microsoft::Console::VirtualTerminal::SgrStack _sgrStack;
#ifdef UNIT_TESTING
friend class TerminalCoreUnitTests::TerminalBufferTests;
friend class TerminalCoreUnitTests::TerminalApiTest;

View file

@ -640,3 +640,31 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept
{
return _workingDirectory;
}
// Method Description:
// - Saves the current text attributes to an internal stack.
// Arguments:
// - options, cOptions: if present, specify which portions of the current text attributes
// should be saved. Only a small subset of GraphicsOptions are actually supported;
// others are ignored. If no options are specified, all attributes are stored.
// Return Value:
// - true
bool Terminal::PushGraphicsRendition(const VTParameters options) noexcept
{
_sgrStack.Push(_buffer->GetCurrentAttributes(), options);
return true;
}
// Method Description:
// - Restores text attributes from the internal stack. If only portions of text attributes
// were saved, combines those with the current attributes.
// Arguments:
// - <none>
// Return Value:
// - true
bool Terminal::PopGraphicsRendition() noexcept
{
const TextAttribute current = _buffer->GetCurrentAttributes();
_buffer->SetCurrentAttributes(_sgrStack.Pop(current));
return true;
}

View file

@ -18,6 +18,9 @@ public:
bool SetGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override;
bool PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) noexcept override;
bool PopGraphicsRendition() noexcept override;
bool CursorPosition(const size_t line,
const size_t column) noexcept override; // CUP

View file

@ -276,3 +276,13 @@ bool TerminalDispatch::SetGraphicsRendition(const VTParameters options) noexcept
_terminalApi.SetTextAttributes(attr);
return true;
}
bool TerminalDispatch::PushGraphicsRendition(const VTParameters options) noexcept
{
return _terminalApi.PushGraphicsRendition(options);
}
bool TerminalDispatch::PopGraphicsRendition() noexcept
{
return _terminalApi.PopGraphicsRendition();
}

View file

@ -54,7 +54,7 @@ std::vector<SMALL_RECT> Terminal::_GetSelectionRects() const noexcept
try
{
return _buffer->GetTextRects(_selection->start, _selection->end, _blockSelection);
return _buffer->GetTextRects(_selection->start, _selection->end, _blockSelection, false);
}
CATCH_LOG();
return result;

View file

@ -20,6 +20,14 @@ using namespace winrt::Windows::Foundation::Collections;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
// The first 8 entries of the color table are non-bright colors, whereas the rest are bright.
static constexpr uint8_t ColorTableDivider{ 8 };
static constexpr std::wstring_view ForegroundColorTag{ L"Foreground" };
static constexpr std::wstring_view BackgroundColorTag{ L"Background" };
static constexpr std::wstring_view CursorColorTag{ L"CursorColor" };
static constexpr std::wstring_view SelectionBackgroundColorTag{ L"SelectionBackground" };
static const std::array<hstring, 16> TableColorNames = {
RS_(L"ColorScheme_Black/Header"),
RS_(L"ColorScheme_Red/Header"),
@ -53,9 +61,25 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
ColorSchemes::ColorSchemes() :
_ColorSchemeList{ single_threaded_observable_vector<Model::ColorScheme>() },
_CurrentColorTable{ single_threaded_observable_vector<Editor::ColorTableEntry>() }
_CurrentNonBrightColorTable{ single_threaded_observable_vector<Editor::ColorTableEntry>() },
_CurrentBrightColorTable{ single_threaded_observable_vector<Editor::ColorTableEntry>() }
{
InitializeComponent();
Automation::AutomationProperties::SetName(ColorSchemeComboBox(), RS_(L"ColorScheme_Name/Header"));
Automation::AutomationProperties::SetFullDescription(ColorSchemeComboBox(), RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
ToolTipService::SetToolTip(ColorSchemeComboBox(), box_value(RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")));
Automation::AutomationProperties::SetName(RenameButton(), RS_(L"Rename/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(NameBox(), RS_(L"ColorScheme_Name/Header"));
Automation::AutomationProperties::SetFullDescription(NameBox(), RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
ToolTipService::SetToolTip(NameBox(), box_value(RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")));
Automation::AutomationProperties::SetName(RenameAcceptButton(), RS_(L"RenameAccept/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(RenameCancelButton(), RS_(L"RenameCancel/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(AddNewButton(), RS_(L"ColorScheme_AddNewButton/Text"));
Automation::AutomationProperties::SetName(DeleteButton(), RS_(L"ColorScheme_DeleteButton/Text"));
}
void ColorSchemes::OnNavigatedTo(const NavigationEventArgs& e)
@ -70,9 +94,20 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
// very accurately.
for (uint8_t i = 0; i < TableColorNames.size(); ++i)
{
auto entry = winrt::make<ColorTableEntry>(i, Windows::UI::Color{ 0, 0, 0, 0 });
_CurrentColorTable.Append(entry);
const auto& entry{ winrt::make<ColorTableEntry>(i, Windows::UI::Color{ 0, 0, 0, 0 }) };
if (i < ColorTableDivider)
{
_CurrentNonBrightColorTable.Append(entry);
}
else
{
_CurrentBrightColorTable.Append(entry);
}
}
_CurrentForegroundColor = winrt::make<ColorTableEntry>(ForegroundColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
_CurrentBackgroundColor = winrt::make<ColorTableEntry>(BackgroundColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
_CurrentCursorColor = winrt::make<ColorTableEntry>(CursorColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
_CurrentSelectionBackgroundColor = winrt::make<ColorTableEntry>(SelectionBackgroundColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
// Try to look up the scheme that was navigated to. If we find it, immediately select it.
const std::wstring lastNameFromNav{ _State.LastSelectedScheme() };
@ -85,6 +120,41 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
auto scheme = *it;
ColorSchemeComboBox().SelectedItem(scheme);
}
// populate color table grid
const auto colorLabelStyle{ Resources().Lookup(winrt::box_value(L"ColorLabelStyle")).as<Windows::UI::Xaml::Style>() };
const auto colorControlStyle{ Resources().Lookup(winrt::box_value(L"ColorControlStyle")).as<Windows::UI::Xaml::Style>() };
const auto colorTableEntryTemplate{ Resources().Lookup(winrt::box_value(L"ColorTableEntryTemplate")).as<DataTemplate>() };
auto setupColorControl = [colorTableEntryTemplate, colorControlStyle, colorTableGrid{ ColorTableGrid() }](const auto&& colorRef, const uint32_t& row, const uint32_t& col) {
ContentControl colorControl{};
colorControl.ContentTemplate(colorTableEntryTemplate);
colorControl.Style(colorControlStyle);
Data::Binding binding{};
binding.Source(colorRef);
binding.Mode(Data::BindingMode::TwoWay);
colorControl.SetBinding(ContentControl::ContentProperty(), binding);
colorTableGrid.Children().Append(colorControl);
Grid::SetRow(colorControl, row);
Grid::SetColumn(colorControl, col);
};
for (uint32_t row = 0; row < ColorTableGrid().RowDefinitions().Size(); ++row)
{
// color label
TextBlock label{};
label.Text(TableColorNames[row]);
label.Style(colorLabelStyle);
ColorTableGrid().Children().Append(label);
Grid::SetRow(label, row);
Grid::SetColumn(label, 0);
// regular color
setupColorControl(_CurrentNonBrightColorTable.GetAt(row), row, 1);
// bright color
setupColorControl(_CurrentBrightColorTable.GetAt(row), row, 2);
}
}
// Function Description:
@ -149,20 +219,52 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void ColorSchemes::ColorPickerChanged(IInspectable const& sender,
ColorChangedEventArgs const& args)
{
if (auto picker = sender.try_as<ColorPicker>())
if (const auto& picker{ sender.try_as<ColorPicker>() })
{
if (auto tag = picker.Tag())
if (const auto& tag{ picker.Tag() })
{
auto index = winrt::unbox_value<uint8_t>(tag);
CurrentColorScheme().SetColorTableEntry(index, args.NewColor());
_CurrentColorTable.GetAt(index).Color(args.NewColor());
if (const auto index{ tag.try_as<uint8_t>() })
{
CurrentColorScheme().SetColorTableEntry(*index, args.NewColor());
if (index < ColorTableDivider)
{
_CurrentNonBrightColorTable.GetAt(*index).Color(args.NewColor());
}
else
{
_CurrentBrightColorTable.GetAt(*index - ColorTableDivider).Color(args.NewColor());
}
}
else if (const auto stringTag{ tag.try_as<hstring>() })
{
if (stringTag == ForegroundColorTag)
{
CurrentColorScheme().Foreground(args.NewColor());
_CurrentForegroundColor.Color(args.NewColor());
}
else if (stringTag == BackgroundColorTag)
{
CurrentColorScheme().Background(args.NewColor());
_CurrentBackgroundColor.Color(args.NewColor());
}
else if (stringTag == CursorColorTag)
{
CurrentColorScheme().CursorColor(args.NewColor());
_CurrentCursorColor.Color(args.NewColor());
}
else if (stringTag == SelectionBackgroundColorTag)
{
CurrentColorScheme().SelectionBackground(args.NewColor());
_CurrentSelectionBackgroundColor.Color(args.NewColor());
}
}
}
}
}
bool ColorSchemes::CanDeleteCurrentScheme() const
{
if (const auto scheme{ CurrentColorScheme() })
if (const auto& scheme{ CurrentColorScheme() })
{
// Only allow this color scheme to be deleted if it's not provided in-box
const std::wstring myName{ scheme.Name() };
@ -295,14 +397,32 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
for (uint8_t i = 0; i < TableColorNames.size(); ++i)
{
_CurrentColorTable.GetAt(i).Color(colorScheme.Table()[i]);
if (i < ColorTableDivider)
{
_CurrentNonBrightColorTable.GetAt(i).Color(colorScheme.Table()[i]);
}
else
{
_CurrentBrightColorTable.GetAt(i - ColorTableDivider).Color(colorScheme.Table()[i]);
}
}
_CurrentForegroundColor.Color(colorScheme.Foreground());
_CurrentBackgroundColor.Color(colorScheme.Background());
_CurrentCursorColor.Color(colorScheme.CursorColor());
_CurrentSelectionBackgroundColor.Color(colorScheme.SelectionBackground());
}
ColorTableEntry::ColorTableEntry(uint8_t index, Windows::UI::Color color)
{
Name(TableColorNames[index]);
Index(winrt::box_value<uint8_t>(index));
Tag(winrt::box_value<uint8_t>(index));
Color(color);
}
ColorTableEntry::ColorTableEntry(std::wstring_view tag, Windows::UI::Color color)
{
Name(LocalizedNameForEnumName(L"ColorScheme_", tag, L"Text"));
Tag(winrt::box_value(tag));
Color(color);
}
}

View file

@ -39,12 +39,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void DeleteConfirmation_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
GETSET_PROPERTY(Editor::ColorSchemesPageNavigationState, State, nullptr);
GETSET_PROPERTY(Windows::Foundation::Collections::IObservableVector<winrt::Microsoft::Terminal::Settings::Editor::ColorTableEntry>, CurrentColorTable, nullptr);
GETSET_PROPERTY(Model::ColorScheme, CurrentColorScheme, nullptr);
GETSET_PROPERTY(Windows::Foundation::Collections::IVector<Editor::ColorTableEntry>, CurrentNonBrightColorTable, nullptr);
GETSET_PROPERTY(Windows::Foundation::Collections::IVector<Editor::ColorTableEntry>, CurrentBrightColorTable, nullptr);
GETSET_PROPERTY(Windows::Foundation::Collections::IObservableVector<Model::ColorScheme>, ColorSchemeList, nullptr);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::Microsoft::Terminal::Settings::Model::ColorScheme, CurrentColorScheme, _PropertyChangedHandlers, nullptr);
OBSERVABLE_GETSET_PROPERTY(bool, IsRenaming, _PropertyChangedHandlers, nullptr);
OBSERVABLE_GETSET_PROPERTY(Editor::ColorTableEntry, CurrentForegroundColor, _PropertyChangedHandlers, nullptr);
OBSERVABLE_GETSET_PROPERTY(Editor::ColorTableEntry, CurrentBackgroundColor, _PropertyChangedHandlers, nullptr);
OBSERVABLE_GETSET_PROPERTY(Editor::ColorTableEntry, CurrentCursorColor, _PropertyChangedHandlers, nullptr);
OBSERVABLE_GETSET_PROPERTY(Editor::ColorTableEntry, CurrentSelectionBackgroundColor, _PropertyChangedHandlers, nullptr);
private:
void _UpdateColorTable(const winrt::Microsoft::Terminal::Settings::Model::ColorScheme& colorScheme);
@ -56,10 +61,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
public:
ColorTableEntry(uint8_t index, Windows::UI::Color color);
ColorTableEntry(std::wstring_view tag, Windows::UI::Color color);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Name, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(IInspectable, Index, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(IInspectable, Tag, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(Windows::UI::Color, Color, _PropertyChangedHandlers);
};
}

View file

@ -18,15 +18,24 @@ namespace Microsoft.Terminal.Settings.Editor
Boolean CanDeleteCurrentScheme { get; };
Boolean IsRenaming { get; };
Microsoft.Terminal.Settings.Model.ColorScheme CurrentColorScheme { get; };
Windows.Foundation.Collections.IObservableVector<ColorTableEntry> CurrentColorTable;
// Terminal Colors
Windows.Foundation.Collections.IVector<ColorTableEntry> CurrentNonBrightColorTable { get; };
Windows.Foundation.Collections.IVector<ColorTableEntry> CurrentBrightColorTable { get; };
// System Colors
ColorTableEntry CurrentForegroundColor;
ColorTableEntry CurrentBackgroundColor;
ColorTableEntry CurrentCursorColor;
ColorTableEntry CurrentSelectionBackgroundColor;
Windows.Foundation.Collections.IObservableVector<Microsoft.Terminal.Settings.Model.ColorScheme> ColorSchemeList { get; };
}
[default_interface] runtimeclass ColorTableEntry : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
String Name { get; };
IInspectable Index;
IInspectable Tag;
Windows.UI.Color Color;
}
}

View file

@ -17,18 +17,25 @@ the MIT License. See LICENSE in the project root for license information. -->
<ResourceDictionary Source="CommonResources.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Thickness x:Key="ColorSchemesStandardControlMargin">0,0,10,20</Thickness>
<Style x:Key="ColorStackPanelStyle" TargetType="StackPanel">
<Setter Property="Margin" Value="{StaticResource ColorSchemesStandardControlMargin}"/>
<Setter Property="Height" Value="59"/>
<Style x:Key="GroupHeaderStyle" TargetType="TextBlock" BasedOn="{StaticResource SubtitleTextBlockStyle}">
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style x:Key="ColorHeaderStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="0,0,0,5"/>
<Style x:Key="ColorLabelStyle" TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Style x:Key="ColorTableGridStyle" TargetType="Grid">
<Setter Property="RowSpacing" Value="10"/>
<Setter Property="ColumnSpacing" Value="10"/>
</Style>
<Style x:Key="ColorControlStyle" TargetType="ContentControl">
<Setter Property="IsTabStop" Value="False"/>
</Style>
<Style x:Key="ColorButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="BorderBrush" Value="{StaticResource SystemBaseLowColor}"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Width" Value="100"/>
@ -42,6 +49,36 @@ the MIT License. See LICENSE in the project root for license information. -->
<Setter Property="IsAlphaTextInputVisible" Value="True"/>
</Style>
<DataTemplate x:Key="ColorTableEntryTemplate" x:DataType="local:ColorTableEntry">
<Button Background="{x:Bind Color, Converter={StaticResource ColorToBrushConverter}, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind Name}"
AutomationProperties.Name="{x:Bind Name}"
Style="{StaticResource ColorButtonStyle}">
<Button.Resources>
<!-- Resources to colorize hover/pressed states based on the color of the button -->
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{x:Bind Color, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{x:Bind Color, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<!-- No High contrast dictionary, let's just leave that unchanged. -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<Button.Flyout>
<Flyout>
<ColorPicker Tag="{x:Bind Tag, Mode=OneWay}"
Color="{x:Bind Color, Mode=OneWay}"
ColorChanged="ColorPickerChanged"/>
</Flyout>
</Button.Flyout>
</Button>
</DataTemplate>
<local:ColorToBrushConverter x:Key="ColorToBrushConverter"/>
<local:ColorToHexConverter x:Key="ColorToHexConverter"/>
<local:InvertedBooleanToVisibilityConverter x:Key="InvertedBooleanToVisibilityConverter"/>
@ -51,22 +88,27 @@ the MIT License. See LICENSE in the project root for license information. -->
</Page.Resources>
<ScrollViewer>
<Grid Margin="{StaticResource StandardIndentMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" MaxWidth="480"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<!--Official guidance states that 640 is the breakpoint between small and medium devices.
Since MinWindowWidth is an inclusive range, we need to add 1 to it.-->
<AdaptiveTrigger MinWindowWidth="641"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ColorPanel.Orientation" Value="Horizontal"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<StackPanel Margin="{StaticResource StandardIndentMargin}"
Spacing="24">
<!--Select Color and Add New Button-->
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal"
Visibility="{x:Bind IsRenaming, Converter={StaticResource InvertedBooleanToVisibilityConverter}, Mode=OneWay}">
@ -111,6 +153,7 @@ the MIT License. See LICENSE in the project root for license information. -->
<!--Accept rename button-->
<Button x:Uid="RenameAccept"
x:Name="RenameAcceptButton"
Style="{StaticResource AccentSmallButtonStyle}"
Click="RenameAccept_Click">
<StackPanel Orientation="Horizontal">
@ -120,6 +163,7 @@ the MIT License. See LICENSE in the project root for license information. -->
</Button>
<!--Cancel rename button-->
<Button x:Uid="RenameCancel"
x:Name="RenameCancelButton"
Style="{StaticResource SmallButtonStyle}"
Click="RenameCancel_Click">
<StackPanel Orientation="Horizontal">
@ -130,7 +174,8 @@ the MIT License. See LICENSE in the project root for license information. -->
</StackPanel>
<!--Add new button-->
<Button Click="AddNew_Click"
<Button x:Name="AddNewButton"
Click="AddNew_Click"
Style="{StaticResource BrowseButtonStyle}">
<StackPanel Orientation="Horizontal">
<FontIcon Glyph="&#xE710;"
@ -141,181 +186,109 @@ the MIT License. See LICENSE in the project root for license information. -->
</Button>
</StackPanel>
<!-- Color Table (Left Column)-->
<ItemsControl x:Name="ColorTableControl"
ItemsSource="{x:Bind CurrentColorTable, Mode=OneWay}"
Grid.Row="1"
Grid.Column="0"
Style="{StaticResource ItemsControlStyle}">
<!-- Terminal Colors (Left Column)-->
<StackPanel x:Name="ColorPanel"
Spacing="48">
<StackPanel>
<TextBlock x:Uid="ColorScheme_TerminalColorsHeader"
Style="{StaticResource GroupHeaderStyle}"/>
<Grid x:Name="ColorTableGrid"
Style="{StaticResource ColorTableGridStyle}">
<Grid.ColumnDefinitions>
<!--Labels-->
<ColumnDefinition Width="Auto"/>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal"
MaximumRowsOrColumns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:ColorTableEntry">
<StackPanel Style="{StaticResource ColorStackPanelStyle}">
<TextBlock Text="{x:Bind Name, Mode=OneWay}"
Style="{StaticResource ColorHeaderStyle}"/>
<Button Background="{x:Bind Color, Converter={StaticResource ColorToBrushConverter}, Mode=OneWay}">
<!--Regular Colors-->
<ColumnDefinition Width="Auto"/>
<!--Bright Colors-->
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button.Resources>
<!-- Resources to colorize hover/pressed states based on the color of the button -->
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{x:Bind Color, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{x:Bind Color, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<!-- No High contrast dictionary, let's just leave that unchanged. -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<Button.Flyout>
<Flyout>
<ColorPicker Tag="{x:Bind Index, Mode=OneWay}"
Color="{x:Bind Color, Mode=OneWay}"
ColorChanged="ColorPickerChanged"/>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Additional Colors (Right Column) -->
<ItemsControl Grid.Row="1"
Grid.Column="1"
Style="{StaticResource ItemsControlStyle}">
<StackPanel Style="{StaticResource ColorStackPanelStyle}">
<TextBlock x:Uid="ColorScheme_Foreground"
Style="{StaticResource ColorHeaderStyle}"/>
<Button Background="{Binding Color, ElementName=ForegroundPicker, Converter={StaticResource ColorToBrushConverter}, Mode=OneWay}">
<Button.Resources>
<!-- Resources to colorize hover/pressed states based on the color of the button -->
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=ForegroundPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=ForegroundPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<!-- No High contrast dictionary, let's just leave that unchanged. -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<Button.Flyout>
<Flyout>
<ColorPicker x:Name="ForegroundPicker" Color="{x:Bind CurrentColorScheme.Foreground, Mode=TwoWay}"/>
</Flyout>
</Button.Flyout>
</Button>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
</Grid>
</StackPanel>
<StackPanel Style="{StaticResource ColorStackPanelStyle}">
<TextBlock x:Uid="ColorScheme_Background"
Style="{StaticResource ColorHeaderStyle}"/>
<Button Background="{Binding Color, ElementName=BackgroundPicker, Converter={StaticResource ColorToBrushConverter}, Mode=OneWay}">
<!-- System Colors (Right Column) -->
<StackPanel>
<TextBlock x:Uid="ColorScheme_SystemColorsHeader"
Style="{StaticResource GroupHeaderStyle}"/>
<Grid Style="{StaticResource ColorTableGridStyle}">
<Button.Resources>
<!-- Resources to colorize hover/pressed states based on the color of the button -->
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=BackgroundPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=BackgroundPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<!-- No High contrast dictionary, let's just leave that unchanged. -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button.Flyout>
<Flyout>
<ColorPicker x:Name="BackgroundPicker"
Color="{x:Bind CurrentColorScheme.Background, Mode=TwoWay}"/>
</Flyout>
</Button.Flyout>
</Button>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!--Foreground-->
<TextBlock x:Uid="ColorScheme_Foreground"
Style="{StaticResource ColorLabelStyle}"
Grid.Row="0"
Grid.Column="0"/>
<ContentControl x:Name="ForegroundButton"
ContentTemplate="{StaticResource ColorTableEntryTemplate}"
Content="{x:Bind CurrentForegroundColor, Mode=TwoWay}"
Style="{StaticResource ColorControlStyle}"
Grid.Row="0"
Grid.Column="1"/>
<!--Background-->
<TextBlock x:Uid="ColorScheme_Background"
Style="{StaticResource ColorLabelStyle}"
Grid.Row="1"
Grid.Column="0"/>
<ContentControl x:Name="BackgroundButton"
ContentTemplate="{StaticResource ColorTableEntryTemplate}"
Content="{x:Bind CurrentBackgroundColor, Mode=TwoWay}"
Style="{StaticResource ColorControlStyle}"
Grid.Row="1"
Grid.Column="1"/>
<!--Cursor Color-->
<TextBlock x:Uid="ColorScheme_CursorColor"
Style="{StaticResource ColorLabelStyle}"
Grid.Row="2"
Grid.Column="0"/>
<ContentControl x:Name="CursorColorButton"
ContentTemplate="{StaticResource ColorTableEntryTemplate}"
Content="{x:Bind CurrentCursorColor, Mode=TwoWay}"
Style="{StaticResource ColorControlStyle}"
Grid.Row="2"
Grid.Column="1"/>
<!--Selection Background-->
<TextBlock x:Uid="ColorScheme_SelectionBackground"
Style="{StaticResource ColorLabelStyle}"
Grid.Row="3"
Grid.Column="0"/>
<ContentControl x:Name="SelectionBackgroundButton"
ContentTemplate="{StaticResource ColorTableEntryTemplate}"
Content="{x:Bind CurrentSelectionBackgroundColor, Mode=TwoWay}"
Style="{StaticResource ColorControlStyle}"
Grid.Row="3"
Grid.Column="1"/>
</Grid>
</StackPanel>
<StackPanel Style="{StaticResource ColorStackPanelStyle}">
<TextBlock x:Uid="ColorScheme_CursorColor"
Style="{StaticResource ColorHeaderStyle}"/>
<Button Background="{Binding Color, ElementName=CursorColorPicker, Converter={StaticResource ColorToBrushConverter}, Mode=OneWay}">
<Button.Resources>
<!-- Resources to colorize hover/pressed states based on the color of the button -->
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=CursorColorPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=CursorColorPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<!-- No High contrast dictionary, let's just leave that unchanged. -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<Button.Flyout>
<Flyout>
<ColorPicker x:Name="CursorColorPicker"
Color="{x:Bind CurrentColorScheme.CursorColor, Mode=TwoWay}"/>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
<StackPanel Style="{StaticResource ColorStackPanelStyle}">
<TextBlock x:Uid="ColorScheme_SelectionBackground"
Style="{StaticResource ColorHeaderStyle}"/>
<Button Background="{Binding Color, ElementName=SelectionBackgroundPicker, Converter={StaticResource ColorToBrushConverter}, Mode=OneWay}">
<Button.Resources>
<!-- Resources to colorize hover/pressed states based on the color of the button -->
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=SelectionBackgroundPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{Binding Color, ElementName=SelectionBackgroundPicker, Converter={StaticResource ColorLightenConverter}, Mode=OneWay}"/>
</ResourceDictionary>
<!-- No High contrast dictionary, let's just leave that unchanged. -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<Button.Flyout>
<Flyout>
<ColorPicker x:Name="SelectionBackgroundPicker"
Color="{x:Bind CurrentColorScheme.SelectionBackground, Mode=TwoWay}"/>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</ItemsControl>
</StackPanel>
<!--Delete Button-->
<StackPanel Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Style="{StaticResource PivotStackStyle}">
<StackPanel Style="{StaticResource PivotStackStyle}">
<Button x:Name="DeleteButton"
IsEnabled="{x:Bind CanDeleteCurrentScheme, Mode=OneWay}"
Style="{StaticResource DeleteButtonStyle}">
@ -371,6 +344,6 @@ the MIT License. See LICENSE in the project root for license information. -->
Style="{StaticResource DisclaimerStyle}"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</Page>

View file

@ -46,6 +46,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_EnumName{ enumName },
_EnumValue{ enumValue } {}
hstring ToString()
{
return EnumName();
}
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, EnumName, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::Windows::Foundation::IInspectable, EnumValue, _PropertyChangedHandlers);

View file

@ -3,7 +3,7 @@
namespace Microsoft.Terminal.Settings.Editor
{
[default_interface] runtimeclass EnumEntry : Windows.UI.Xaml.Data.INotifyPropertyChanged
[default_interface] runtimeclass EnumEntry : Windows.UI.Xaml.Data.INotifyPropertyChanged, Windows.Foundation.IStringable
{
String EnumName { get; };
IInspectable EnumValue { get; };

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -831,4 +831,10 @@
<data name="Globals_FocusFollowMouse.HelpText" xml:space="preserve">
<value>When checked, the terminal will the focus pane on mouse hover.</value>
</data>
</root>
<data name="ColorScheme_TerminalColorsHeader.Text" xml:space="preserve">
<value>Terminal colors</value>
</data>
<data name="ColorScheme_SystemColorsHeader.Text" xml:space="preserve">
<value>System colors</value>
</data>
</root>

View file

@ -51,6 +51,7 @@ static constexpr std::string_view LegacyToggleRetroEffectKey{ "toggleRetroEffect
static constexpr std::string_view ToggleShaderEffectsKey{ "toggleShaderEffects" };
static constexpr std::string_view MoveTabKey{ "moveTab" };
static constexpr std::string_view BreakIntoDebuggerKey{ "breakIntoDebugger" };
static constexpr std::string_view FindMatchKey{ "findMatch" };
static constexpr std::string_view TogglePaneReadOnlyKey{ "toggleReadOnlyMode" };
static constexpr std::string_view ActionKey{ "action" };
@ -116,6 +117,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ MoveTabKey, ShortcutAction::MoveTab },
{ BreakIntoDebuggerKey, ShortcutAction::BreakIntoDebugger },
{ UnboundKey, ShortcutAction::Invalid },
{ FindMatchKey, ShortcutAction::FindMatch },
{ TogglePaneReadOnlyKey, ShortcutAction::TogglePaneReadOnly },
};
@ -147,6 +149,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::ScrollDown, ScrollDownArgs::FromJson },
{ ShortcutAction::MoveTab, MoveTabArgs::FromJson },
{ ShortcutAction::ToggleCommandPalette, ToggleCommandPaletteArgs::FromJson },
{ ShortcutAction::FindMatch, FindMatchArgs::FromJson },
{ ShortcutAction::Invalid, nullptr },
};
@ -316,6 +319,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::ToggleShaderEffects, RS_(L"ToggleShaderEffectsCommandKey") },
{ ShortcutAction::MoveTab, L"" }, // Intentionally omitted, must be generated by GenerateName
{ ShortcutAction::BreakIntoDebugger, RS_(L"BreakIntoDebuggerCommandKey") },
{ ShortcutAction::FindMatch, L"" }, // Intentionally omitted, must be generated by GenerateName
{ ShortcutAction::TogglePaneReadOnly, RS_(L"TogglePaneReadOnlyCommandKey") },
};
}();

View file

@ -23,6 +23,7 @@
#include "CloseOtherTabsArgs.g.cpp"
#include "CloseTabsAfterArgs.g.cpp"
#include "MoveTabArgs.g.cpp"
#include "FindMatchArgs.g.cpp"
#include "ToggleCommandPaletteArgs.g.cpp"
#include <LibraryResources.h>
@ -431,4 +432,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
return RS_(L"ToggleCommandPaletteCommandKey");
}
winrt::hstring FindMatchArgs::GenerateName() const
{
switch (_Direction)
{
case FindMatchDirection::Next:
return winrt::hstring{ RS_(L"FindNextCommandKey") };
case FindMatchDirection::Previous:
return winrt::hstring{ RS_(L"FindPrevCommandKey") };
}
return L"";
}
}

View file

@ -26,6 +26,7 @@
#include "ScrollDownArgs.g.h"
#include "MoveTabArgs.g.h"
#include "ToggleCommandPaletteArgs.g.h"
#include "FindMatchArgs.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
#include "JsonUtils.h"
@ -838,6 +839,50 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return *copy;
}
};
struct FindMatchArgs : public FindMatchArgsT<FindMatchArgs>
{
FindMatchArgs() = default;
FindMatchArgs(FindMatchDirection direction) :
_Direction{ direction } {};
GETSET_PROPERTY(FindMatchDirection, Direction, FindMatchDirection::None);
static constexpr std::string_view DirectionKey{ "direction" };
public:
hstring GenerateName() const;
bool Equals(const IActionArgs& other)
{
auto otherAsUs = other.try_as<FindMatchArgs>();
if (otherAsUs)
{
return otherAsUs->_Direction == _Direction;
}
return false;
};
static FromJsonResult FromJson(const Json::Value& json)
{
// LOAD BEARING: Not using make_self here _will_ break you in the future!
auto args = winrt::make_self<FindMatchArgs>();
JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction);
if (args->_Direction == FindMatchDirection::None)
{
return { nullptr, { SettingsLoadWarnings::MissingRequiredParameter } };
}
else
{
return { *args, {} };
}
}
IActionArgs Copy() const
{
auto copy{ winrt::make_self<FindMatchArgs>() };
copy->_Direction = _Direction;
return *copy;
}
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
@ -853,4 +898,5 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
BASIC_FACTORY(CloseTabsAfterArgs);
BASIC_FACTORY(MoveTabArgs);
BASIC_FACTORY(OpenSettingsArgs);
BASIC_FACTORY(FindMatchArgs);
}

View file

@ -64,6 +64,13 @@ namespace Microsoft.Terminal.Settings.Model
Backward
};
enum FindMatchDirection
{
None = 0,
Next,
Previous
};
enum CommandPaletteLaunchMode
{
Action = 0,
@ -204,4 +211,10 @@ namespace Microsoft.Terminal.Settings.Model
{
CommandPaletteLaunchMode LaunchMode { get; };
};
[default_interface] runtimeclass FindMatchArgs : IActionArgs
{
FindMatchArgs(FindMatchDirection direction);
FindMatchDirection Direction { get; };
};
}

View file

@ -117,6 +117,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::com_ptr<implementation::Profile> _FindMatchingProfile(const Json::Value& profileJson);
std::optional<uint32_t> _FindMatchingProfileIndex(const Json::Value& profileJson);
void _LayerOrCreateColorScheme(const Json::Value& schemeJson);
Json::Value _ParseUtf8JsonString(std::string_view fileData);
winrt::com_ptr<implementation::ColorScheme> _FindMatchingColorScheme(const Json::Value& schemeJson);
void _ParseJsonString(std::string_view fileData, const bool isDefaultSettings);
static const Json::Value& _GetProfilesJsonObject(const Json::Value& json);
@ -129,6 +131,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _ApplyDefaultsFromUserSettings();
void _LoadDynamicProfiles();
void _LoadFragmentExtensions();
void _ApplyJsonStubsHelper(const std::wstring_view directory, const std::unordered_set<std::wstring>& ignoredNamespaces);
std::unordered_set<std::string> _AccumulateJsonFilesInDirectory(const std::wstring_view directory);
void _ParseAndLayerFragmentFiles(const std::unordered_set<std::string> files, const winrt::hstring source);
static bool _IsPackaged();
static void _WriteSettings(std::string_view content, const hstring filepath);

View file

@ -10,6 +10,7 @@
#include <appmodel.h>
#include <shlobj.h>
#include <fmt/chrono.h>
#include "DefaultProfileUtils.h"
// defaults.h is a file containing the default json settings in a std::string_view
#include "defaults.h"
@ -36,6 +37,9 @@ static constexpr std::string_view ProfilesListKey{ "list" };
static constexpr std::string_view LegacyKeybindingsKey{ "keybindings" };
static constexpr std::string_view ActionsKey{ "actions" };
static constexpr std::string_view SchemesKey{ "schemes" };
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view UpdatesKey{ "updates" };
static constexpr std::string_view GuidKey{ "guid" };
static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" };
@ -43,6 +47,39 @@ static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
static constexpr std::string_view SettingsSchemaFragment{ "\n"
R"( "$schema": "https://aka.ms/terminal-profiles-schema")" };
static constexpr std::string_view jsonExtension{ ".json" };
static constexpr std::string_view FragmentsSubDirectory{ "\\Fragments" };
static constexpr std::wstring_view FragmentsPath{ L"\\Microsoft\\Windows Terminal\\Fragments" };
static constexpr std::string_view AppExtensionHostName{ "com.microsoft.windows.terminal.settings" };
// Function Description:
// - Extracting the value from an async task (like talking to the app catalog) when we are on the
// UI thread causes C++/WinRT to complain quite loudly (and halt execution!)
// This templated function extracts the result from a task with chicanery.
template<typename TTask>
static auto _extractValueFromTaskWithoutMainThreadAwait(TTask&& task) -> decltype(task.get())
{
using TVal = decltype(task.get());
std::optional<TVal> finalVal{};
std::condition_variable cv;
std::mutex mtx;
auto waitOnBackground = [&]() -> winrt::fire_and_forget {
co_await winrt::resume_background();
auto v{ co_await task };
std::unique_lock<std::mutex> lock{ mtx };
finalVal.emplace(std::move(v));
cv.notify_all();
};
std::unique_lock<std::mutex> lock{ mtx };
waitOnBackground();
cv.wait(lock, [&]() { return finalVal.has_value(); });
return *finalVal;
}
static std::tuple<size_t, size_t> _LineAndColumnFromPosition(const std::string_view string, ptrdiff_t position)
{
size_t line = 1, column = position + 1;
@ -136,6 +173,11 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings::
// created by now, because we're going to check in there for any generators
// that should be disabled (if the user had any settings.)
resultPtr->_LoadDynamicProfiles();
try
{
resultPtr->_LoadFragmentExtensions();
}
CATCH_LOG();
if (!fileHasData)
{
@ -383,6 +425,216 @@ void CascadiaSettings::_LoadDynamicProfiles()
}
}
// Method Description:
// - Searches the local app data folder, global app data folder and app
// extensions for json stubs we should use to create new profiles,
// modify existing profiles or add new color schemes
// - If the user settings has any namespaces in the "disabledProfileSources"
// property, we'll ensure that the corresponding folders do not get searched
void CascadiaSettings::_LoadFragmentExtensions()
{
// First, accumulate the namespaces the user wants to ignore
std::unordered_set<std::wstring> ignoredNamespaces;
const auto disabledProfileSources = CascadiaSettings::_GetDisabledProfileSourcesJsonObject(_userSettings);
if (disabledProfileSources.isArray())
{
for (const auto& json : disabledProfileSources)
{
ignoredNamespaces.emplace(JsonUtils::GetValue<std::wstring>(json));
}
}
// Search through the local app data folder
wil::unique_cotaskmem_string localAppDataFolder;
THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataFolder));
auto localAppDataFragments = std::wstring(localAppDataFolder.get()) + FragmentsPath.data();
if (std::filesystem::exists(localAppDataFragments))
{
_ApplyJsonStubsHelper(localAppDataFragments, ignoredNamespaces);
}
// Search through the program data folder
wil::unique_cotaskmem_string programDataFolder;
THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_ProgramData, 0, nullptr, &programDataFolder));
auto programDataFragments = std::wstring(programDataFolder.get()) + FragmentsPath.data();
if (std::filesystem::exists(programDataFragments))
{
_ApplyJsonStubsHelper(programDataFragments, ignoredNamespaces);
}
// Search through app extensions
// Gets the catalog of extensions with the name "com.microsoft.windows.terminal.settings"
const auto catalog = Windows::ApplicationModel::AppExtensions::AppExtensionCatalog::Open(winrt::to_hstring(AppExtensionHostName));
auto extensions = _extractValueFromTaskWithoutMainThreadAwait(catalog.FindAllAsync());
for (const auto& ext : extensions)
{
// Only apply the stubs if the package name is not in ignored namespaces
if (ignoredNamespaces.find(ext.Package().Id().FamilyName().c_str()) == ignoredNamespaces.end())
{
// Likewise, getting the public folder from an extension is an async operation
// So we use another mutex and condition variable
auto foundFolder = _extractValueFromTaskWithoutMainThreadAwait(ext.GetPublicFolderAsync());
// the StorageFolder class has its own methods for obtaining the files within the folder
// however, all those methods are Async methods
// you may have noticed that we need to resort to clunky implementations for async operations
// (they are in _extractValueFromTaskWithoutMainThreadAwait)
// so for now we will just take the folder path and access the files that way
auto path = winrt::to_string(foundFolder.Path());
path.append(FragmentsSubDirectory);
// If the directory exists, use the fragments in it
if (std::filesystem::exists(path))
{
const auto jsonFiles = _AccumulateJsonFilesInDirectory(til::u8u16(path));
// Provide the package name as the source
_ParseAndLayerFragmentFiles(jsonFiles, ext.Package().Id().FamilyName().c_str());
}
}
}
}
// Method Description:
// - Helper function to apply json stubs in the local app data folder and the global program data folder
// Arguments:
// - The directory to find json files in
// - The set of ignored namespaces
void CascadiaSettings::_ApplyJsonStubsHelper(const std::wstring_view directory, const std::unordered_set<std::wstring>& ignoredNamespaces)
{
// The json files should be within subdirectories where the subdirectory name is the app name
for (const auto& fragmentExtFolder : std::filesystem::directory_iterator(directory))
{
// We only want the parent folder name as the source (not the full path)
const auto source = fragmentExtFolder.path().filename().wstring();
// Only apply the stubs if the parent folder name is not in ignored namespaces
// (also make sure this is a directory for sanity)
if (std::filesystem::is_directory(fragmentExtFolder) && ignoredNamespaces.find(source) == ignoredNamespaces.end())
{
const auto jsonFiles = _AccumulateJsonFilesInDirectory(fragmentExtFolder.path().c_str());
_ParseAndLayerFragmentFiles(jsonFiles, winrt::hstring{ source });
}
}
}
// Method Description:
// - Finds all the json files within the given directory
// Arguments:
// - directory: the directory to search
// Return Value:
// - A set containing all the found file data
std::unordered_set<std::string> CascadiaSettings::_AccumulateJsonFilesInDirectory(const std::wstring_view directory)
{
std::unordered_set<std::string> jsonFiles;
for (const auto& fragmentExt : std::filesystem::directory_iterator(directory))
{
if (fragmentExt.path().extension() == jsonExtension)
{
wil::unique_hfile hFile{ CreateFileW(fragmentExt.path().c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr) };
if (!hFile)
{
LOG_LAST_ERROR();
}
else
{
const auto fileData = _ReadFile(hFile.get()).value();
jsonFiles.emplace(fileData);
}
}
}
return jsonFiles;
}
// Method Description:
// - Given a set of json files, uses them to modify existing profiles,
// create new profiles, and create new color schemes
// Arguments:
// - files: the set of json files (each item in the set is the file data)
// - source: the location the files came from
void CascadiaSettings::_ParseAndLayerFragmentFiles(const std::unordered_set<std::string> files, const winrt::hstring source)
{
for (const auto& file : files)
{
// A file could have many new profiles/many profiles it wants to modify/many new color schemes
// so we first parse the entire file into one json object
auto fullFile = _ParseUtf8JsonString(file.data());
if (fullFile.isMember(JsonKey(ProfilesKey)))
{
// Now we separately get each stub that modifies/adds a profile
// We intentionally don't use a const reference here because we modify
// the profile stub by giving it a guid so we can call _FindMatchingProfile
for (auto& profileStub : fullFile[JsonKey(ProfilesKey)])
{
if (profileStub.isMember(JsonKey(UpdatesKey)))
{
// This stub is meant to be a modification to an existing profile,
// try to find the matching profile
profileStub[JsonKey(GuidKey)] = profileStub[JsonKey(UpdatesKey)];
auto matchingProfile = _FindMatchingProfile(profileStub);
if (matchingProfile)
{
// We found a matching profile, create a child of it and put the modifications there
// (we add a new inheritance layer)
auto childImpl{ matchingProfile->CreateChild() };
childImpl->LayerJson(profileStub);
// replace parent in _profiles with child
_allProfiles.SetAt(_FindMatchingProfileIndex(matchingProfile->ToJson()).value(), *childImpl);
}
}
else
{
// This is a new profile, check that it meets our minimum requirements first
// (it must have at least a name)
if (profileStub.isMember(JsonKey(NameKey)))
{
auto newProfile = Profile::FromJson(profileStub);
// Make sure to give the new profile a source, then we add it to our list of profiles
// We don't make modifications to the user's settings file yet, that will happen when
// _AppendDynamicProfilesToUserSettings() is called later
newProfile->Source(source);
_allProfiles.Append(*newProfile);
}
}
}
}
if (fullFile.isMember(JsonKey(SchemesKey)))
{
// Now we separately get each stub that adds a color scheme
for (const auto& schemeStub : fullFile[JsonKey(SchemesKey)])
{
if (_FindMatchingColorScheme(schemeStub))
{
// We do not allow modifications to existing color schemes
}
else
{
// This is a new color scheme, add it only if it specifies _all_ the fields
if (ColorScheme::ValidateColorScheme(schemeStub))
{
const auto newScheme = ColorScheme::FromJson(schemeStub);
_globals->AddColorScheme(*newScheme);
}
}
}
}
}
}
// Method Description:
// - Attempts to read the given data as a string of JSON and parse that JSON
// into a Json::Value.
@ -400,6 +652,33 @@ void CascadiaSettings::_LoadDynamicProfiles()
// - <none>
void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool isDefaultSettings)
{
// Parse the json data into either our defaults or user settings. We'll keep
// these original json values around for later, in case we need to parse
// their raw contents again.
Json::Value& root = isDefaultSettings ? _defaultSettings : _userSettings;
root = _ParseUtf8JsonString(fileData);
// If this is the user settings, also store away the original settings
// string. We'll need to keep it around so we can modify it without
// re-serializing their settings.
if (!isDefaultSettings)
{
_userSettingsString = fileData;
}
}
// Method Description:
// - Attempts to read the given data as a string of JSON and parse that JSON
// into a Json::Value
// - Will ignore leading UTF-8 BOMs
// Arguments:
// - fileData: the string to parse as JSON data
// Return value:
// - the parsed json value
Json::Value CascadiaSettings::_ParseUtf8JsonString(std::string_view fileData)
{
Json::Value result;
// Ignore UTF-8 BOM
auto actualDataStart = fileData.data();
const auto actualDataEnd = fileData.data() + fileData.size();
@ -411,25 +690,14 @@ void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool is
std::string errs; // This string will receive any error text from failing to parse.
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
// Parse the json data into either our defaults or user settings. We'll keep
// these original json values around for later, in case we need to parse
// their raw contents again.
Json::Value& root = isDefaultSettings ? _defaultSettings : _userSettings;
// `parse` will return false if it fails.
if (!reader->parse(actualDataStart, actualDataEnd, &root, &errs))
if (!reader->parse(actualDataStart, actualDataEnd, &result, &errs))
{
// This will be caught by App::_TryLoadSettings, who will display
// the text to the user.
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
// If this is the user settings, also store away the original settings
// string. We'll need to keep it around so we can modify it without
// re-serializing their settings.
if (!isDefaultSettings)
{
_userSettingsString = fileData;
}
return result;
}
// Method Description:
@ -535,6 +803,7 @@ bool CascadiaSettings::_AppendDynamicProfilesToUserSettings()
// changes to re-create this profile.
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto diff = profileImpl->GenerateStub();
auto profileSerialization = Json::writeString(wbuilder, diff);
// Add the user's indent to the start of each line

View file

@ -173,6 +173,29 @@ void ColorScheme::SetColorTableEntry(uint8_t index, const winrt::Windows::UI::Co
_table[index] = value;
}
// Method Description:
// - Validates a given color scheme
// - A color scheme is valid if it has a name and defines all the colors
// Arguments:
// - The color scheme to validate
// Return Value:
// - true if the scheme is valid, false otherwise
bool ColorScheme::ValidateColorScheme(const Json::Value& scheme)
{
for (const auto& key : TableColors)
{
if (!scheme.isMember(JsonKey(key)))
{
return false;
}
}
if (!scheme.isMember(JsonKey(NameKey)))
{
return false;
}
return true;
}
// Method Description:
// - Parse the name from the JSON representation of a ColorScheme.
// Arguments:

View file

@ -37,6 +37,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ColorScheme(hstring name, COLORREF defaultFg, COLORREF defaultBg, COLORREF cursorColor);
com_ptr<ColorScheme> Copy() const;
hstring ToString()
{
return Name();
}
static com_ptr<ColorScheme> FromJson(const Json::Value& json);
bool ShouldBeLayered(const Json::Value& json) const;
void LayerJson(const Json::Value& json);
@ -48,6 +53,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
com_array<Windows::UI::Color> Table() const noexcept;
void SetColorTableEntry(uint8_t index, const winrt::Windows::UI::Color& value) noexcept;
static bool ValidateColorScheme(const Json::Value& scheme);
GETSET_PROPERTY(winrt::hstring, Name);
GETSET_COLORPROPERTY(Foreground); // defined in constructor
GETSET_COLORPROPERTY(Background); // defined in constructor

View file

@ -3,7 +3,7 @@
namespace Microsoft.Terminal.Settings.Model
{
[default_interface] runtimeclass ColorScheme {
[default_interface] runtimeclass ColorScheme : Windows.Foundation.IStringable {
ColorScheme(String name);
String Name;

View file

@ -53,6 +53,7 @@ namespace Microsoft.Terminal.Settings.Model
TabSearch,
MoveTab,
BreakIntoDebugger,
FindMatch,
TogglePaneReadOnly
};

View file

@ -47,6 +47,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
Profile();
Profile(guid guid);
hstring ToString()
{
return Name();
}
static com_ptr<Profile> CloneInheritanceGraph(com_ptr<Profile> oldProfile, com_ptr<Profile> newProfile, std::unordered_map<void*, com_ptr<Profile>>& visited);
static com_ptr<Profile> CopySettings(com_ptr<Profile> source);

View file

@ -31,7 +31,7 @@ namespace Microsoft.Terminal.Settings.Model
Vertical_Bottom = 0x20
};
[default_interface] runtimeclass Profile {
[default_interface] runtimeclass Profile : Windows.Foundation.IStringable {
Profile();
Profile(Guid guid);

View file

@ -215,6 +215,12 @@
<data name="FindCommandKey" xml:space="preserve">
<value>Find</value>
</data>
<data name="FindNextCommandKey" xml:space="preserve">
<value>Find next search match</value>
</data>
<data name="FindPrevCommandKey" xml:space="preserve">
<value>Find previous search match</value>
</data>
<data name="IncreaseFontSizeCommandKey" xml:space="preserve">
<value>Increase font size</value>
</data>

View file

@ -431,3 +431,11 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::CommandPaletteLa
pair_type{ "commandLine", ValueType::CommandLine },
};
};
JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FindMatchDirection)
{
JSON_MAPPINGS(2) = {
pair_type{ "next", ValueType::Next },
pair_type{ "prev", ValueType::Previous },
};
};

View file

@ -287,6 +287,8 @@
{ "command": "openSettings", "keys": "ctrl+," },
{ "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," },
{ "command": "find", "keys": "ctrl+shift+f" },
{ "command": { "action": "findMatch", "direction": "next" } },
{ "command": { "action": "findMatch", "direction": "prev" } },
{ "command": "toggleShaderEffects" },
{ "command": "openTabColorPicker" },
{ "command": "renameTab" },

View file

@ -30,11 +30,13 @@
#include <hstring.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.AppExtensions.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Media.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.System.h>

View file

@ -34,6 +34,7 @@ Author(s):
#include <WexTestClass.h>
#include "consoletaeftemplates.hpp"
#include "winrtTaefTemplates.hpp"
#include <winrt/Windows.system.h>
#include <winrt/Windows.Foundation.h>

View file

@ -45,6 +45,8 @@ Author(s):
#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Text.h>
#include "winrtTaefTemplates.hpp"
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"

View file

@ -67,7 +67,8 @@ void CursorBlinker::FocusStart()
// - <none>
void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo)
{
Cursor& cursor = ScreenInfo.GetTextBuffer().GetCursor();
auto& buffer = ScreenInfo.GetTextBuffer();
auto& cursor = buffer.GetCursor();
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto* const _pAccessibilityNotifier = ServiceLocator::LocateAccessibilityNotifier();
@ -79,7 +80,9 @@ void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo)
// Update the cursor pos in USER so accessibility will work.
if (cursor.HasMoved())
{
const auto position = cursor.GetPosition();
// Convert the buffer position to the equivalent screen coordinates
// required by the notifier, taking line rendition into account.
const auto position = buffer.BufferToScreenPosition(cursor.GetPosition());
const auto viewport = ScreenInfo.GetViewport();
const auto fontSize = ScreenInfo.GetScreenFontSize();
cursor.SetHasMoved(false);

View file

@ -353,7 +353,12 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
const wchar_t* lpString = pwchRealUnicode;
const COORD coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions();
COORD coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions();
// In VT mode, the width at which we wrap is determined by the line rendition attribute.
if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING))
{
coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y);
}
while (*pcb < BufferSize)
{
@ -371,6 +376,11 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
CursorPosition = cursor.GetPosition();
// In VT mode, we need to recalculate the width when moving to a new line.
if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING))
{
coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y);
}
}
}

View file

@ -422,6 +422,10 @@ void ConsoleImeInfo::_WriteUndeterminedChars(const std::wstring_view text,
// screen buffer and viewport positioning.
// Each conversion area write will adjust these to set up any subsequent calls to go onto the next line.
auto pos = screenInfo.GetTextBuffer().GetCursor().GetPosition();
// Convert the cursor buffer position to the equivalent screen
// coordinates, taking line rendition into account.
pos = screenInfo.GetTextBuffer().BufferToScreenPosition(pos);
const auto view = screenInfo.GetViewport();
// Set cursor position relative to viewport

View file

@ -704,13 +704,18 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
buffer.GetViewport().ToInclusive();
COORD delta{ 0 };
{
if (currentViewport.Left > position.X)
// When evaluating the X offset, we must convert the buffer position to
// equivalent screen coordinates, taking line rendition into account.
const auto lineRendition = buffer.GetTextBuffer().GetLineRendition(position.Y);
const auto screenPosition = BufferToScreenLine({ position.X, position.Y, position.X, position.Y }, lineRendition);
if (currentViewport.Left > screenPosition.Left)
{
delta.X = position.X - currentViewport.Left;
delta.X = screenPosition.Left - currentViewport.Left;
}
else if (currentViewport.Right < position.X)
else if (currentViewport.Right < screenPosition.Right)
{
delta.X = position.X - currentViewport.Right;
delta.X = screenPosition.Right - currentViewport.Right;
}
if (currentViewport.Top > position.Y)
@ -1410,6 +1415,10 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool
{
cursorPosition.X = 0;
}
else
{
cursorPosition = textBuffer.ClampPositionWithinLine(cursorPosition);
}
return AdjustCursorPosition(screenInfo, cursorPosition, FALSE, nullptr);
}
@ -1427,7 +1436,8 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool
const SMALL_RECT viewport = screenInfo.GetActiveBuffer().GetViewport().ToInclusive();
const COORD oldCursorPosition = screenInfo.GetTextBuffer().GetCursor().GetPosition();
const COORD newCursorPosition = { oldCursorPosition.X, oldCursorPosition.Y - 1 };
COORD newCursorPosition = { oldCursorPosition.X, oldCursorPosition.Y - 1 };
newCursorPosition = screenInfo.GetTextBuffer().ClampPositionWithinLine(newCursorPosition);
// If the cursor is at the top of the viewport, we don't want to shift the viewport up.
// We want it to stay exactly where it is.

View file

@ -451,6 +451,13 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo,
{
const auto& view = remaining.at(i);
screenInfo.WriteRect(fillData, view);
// If we're scrolling an area that encompasses the full buffer width,
// then the filled rows should also have their line rendition reset.
if (view.Width() == buffer.Width() && destinationOriginGiven.X == 0)
{
screenInfo.GetTextBuffer().ResetLineRenditionRange(view.Top(), view.BottomExclusive());
}
}
}

View file

@ -125,7 +125,9 @@ bool ConhostInternalGetSet::SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BU
// - true if successful (see DoSrvSetConsoleCursorPosition). false otherwise.
bool ConhostInternalGetSet::SetConsoleCursorPosition(const COORD position)
{
return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorPositionImpl(_io.GetActiveOutputBuffer(), position));
auto& info = _io.GetActiveOutputBuffer();
const auto clampedPosition = info.GetTextBuffer().ClampPositionWithinLine(position);
return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorPositionImpl(info, clampedPosition));
}
// Routine Description:
@ -182,6 +184,48 @@ bool ConhostInternalGetSet::PrivateSetTextAttributes(const TextAttribute& attrs)
return true;
}
// Method Description:
// - Sets the line rendition attribute for the current row of the active screen
// buffer. This controls how character cells are scaled when the row is rendered.
// Arguments:
// - lineRendition: The new LineRendition attribute to use
// Return Value:
// - true if successful. false otherwise.
bool ConhostInternalGetSet::PrivateSetCurrentLineRendition(const LineRendition lineRendition)
{
auto& textBuffer = _io.GetActiveOutputBuffer().GetTextBuffer();
textBuffer.SetCurrentLineRendition(lineRendition);
return true;
}
// Method Description:
// - Resets the line rendition attribute to SingleWidth for a specified range
// of row numbers.
// Arguments:
// - startRow: The row number of first line to be modified
// - endRow: The row number following the last line to be modified
// Return Value:
// - true if successful. false otherwise.
bool ConhostInternalGetSet::PrivateResetLineRenditionRange(const size_t startRow, const size_t endRow)
{
auto& textBuffer = _io.GetActiveOutputBuffer().GetTextBuffer();
textBuffer.ResetLineRenditionRange(startRow, endRow);
return true;
}
// Method Description:
// - Returns the number of cells that will fit on the specified row when
// rendered with its current line rendition.
// Arguments:
// - row: The row number of the line to measure
// Return Value:
// - the number of cells that will fit on the line
SHORT ConhostInternalGetSet::PrivateGetLineWidth(const size_t row) const
{
const auto& textBuffer = _io.GetActiveOutputBuffer().GetTextBuffer();
return textBuffer.GetLineWidth(row);
}
// Routine Description:
// - Connects the WriteConsoleInput API call directly into our Driver Message servicing call inside Conhost.exe
// Arguments:

View file

@ -65,6 +65,10 @@ public:
bool PrivateGetTextAttributes(TextAttribute& attrs) const override;
bool PrivateSetTextAttributes(const TextAttribute& attrs) override;
bool PrivateSetCurrentLineRendition(const LineRendition lineRendition) override;
bool PrivateResetLineRenditionRange(const size_t startRow, const size_t endRow) override;
SHORT PrivateGetLineWidth(const size_t row) const override;
bool PrivateWriteConsoleInputW(std::deque<std::unique_ptr<IInputEvent>>& events,
size_t& eventsWritten) override;

View file

@ -2223,6 +2223,9 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport,
auto fillData = OutputCellIterator{ fillAttributes, fillLength };
Write(fillData, fillPosition, false);
// Also reset the line rendition for the erased rows.
_textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive());
return S_OK;
}
@ -2395,7 +2398,9 @@ void SCREEN_INFORMATION::ClearTextData()
// - word boundary positions
std::pair<COORD, COORD> SCREEN_INFORMATION::GetWordBoundary(const COORD position) const
{
COORD clampedPosition = position;
// The position argument is in screen coordinates, but we need the
// equivalent buffer position, taking line rendition into account.
COORD clampedPosition = _textBuffer->ScreenToBufferPosition(position);
GetBufferSize().Clamp(clampedPosition);
COORD start{ clampedPosition };
@ -2464,6 +2469,12 @@ std::pair<COORD, COORD> SCREEN_INFORMATION::GetWordBoundary(const COORD position
}
}
}
// The calculated range is in buffer coordinates, but the caller is
// expecting screen offsets, so we have to convert these back again.
start = _textBuffer->BufferToScreenPosition(start);
end = _textBuffer->BufferToScreenPosition(end);
return { start, end };
}
@ -2541,6 +2552,8 @@ void SCREEN_INFORMATION::InitializeCursorRowAttributes()
auto fillAttributes = GetAttributes();
fillAttributes.SetStandardErase();
row.GetAttrRow().SetAttrToEnd(0, fillAttributes);
// The row should also be single width to start with.
row.SetLineRendition(LineRendition::SingleWidth);
}
}

View file

@ -59,7 +59,7 @@ std::vector<SMALL_RECT> Selection::GetSelectionRects() const
endSelectionAnchor.Y = (_coordSelectionAnchor.Y == _srSelectionRect.Top) ? _srSelectionRect.Bottom : _srSelectionRect.Top;
const auto blockSelection = !IsLineSelection();
return screenInfo.GetTextBuffer().GetTextRects(_coordSelectionAnchor, endSelectionAnchor, blockSelection);
return screenInfo.GetTextBuffer().GetTextRects(_coordSelectionAnchor, endSelectionAnchor, blockSelection, false);
}
// Routine Description:
@ -417,7 +417,7 @@ void Selection::ColorSelection(const COORD coordSelectionStart, const COORD coor
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto& screenInfo = gci.GetActiveOutputBuffer();
const auto rectangles = screenInfo.GetTextBuffer().GetTextRects(coordSelectionStart, coordSelectionEnd);
const auto rectangles = screenInfo.GetTextBuffer().GetTextRects(coordSelectionStart, coordSelectionEnd, false, true);
for (const auto& rect : rectangles)
{
ColorSelection(rect, attr);

View file

@ -333,7 +333,7 @@ class SelectionTests
COORD startPos{ sTargetX, sTargetY };
COORD endPos{ base::ClampAdd(sTargetX, sLength), sTargetY };
const auto selectionRects = screenInfo.GetTextBuffer().GetTextRects(startPos, endPos);
const auto selectionRects = screenInfo.GetTextBuffer().GetTextRects(startPos, endPos, false, false);
VERIFY_ARE_EQUAL(static_cast<size_t>(1), selectionRects.size());
srSelection = selectionRects.at(0);

View file

@ -2324,7 +2324,7 @@ void TextBufferTests::GetTextRects()
COORD start{ 1, 0 };
COORD end{ 7, 4 };
const auto result = _buffer->GetTextRects(start, end, blockSelection);
const auto result = _buffer->GetTextRects(start, end, blockSelection, false);
VERIFY_ARE_EQUAL(expected.size(), result.size());
for (size_t i = 0; i < expected.size(); ++i)
{
@ -2370,7 +2370,7 @@ void TextBufferTests::GetText()
WriteLinesToBuffer(bufferText, *_buffer);
// simulate a selection from origin to {4,4}
const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 4 }, blockSelection);
const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 4 }, blockSelection, false);
std::wstring result = L"";
const auto textData = _buffer->GetText(includeCRLF, trimTrailingWhitespace, textRects).text;
@ -2471,7 +2471,7 @@ void TextBufferTests::GetText()
// |_____|
// simulate a selection from origin to {4,5}
const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 5 }, blockSelection);
const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 5 }, blockSelection, false);
std::wstring result = L"";

View file

@ -22,6 +22,25 @@ Revision History:
type identifer; \
VERIFY_SUCCEEDED(TestData::TryGetValue(L#identifer, identifer), description);
// Thinking of adding a new VerifyOutputTraits for a new type? MAKE SURE that
// you include this header (or at least the relevant definition) before _every_
// Verify for that type.
//
// From a thread on the matter in 2018:
// > my best guess is that one of your cpp files uses a COORD in a Verify macro
// > without including consoletaeftemplates.hpp. This caused the
// > VerifyOutputTraits<COORD> symbol to be used with the default
// > implementation. In other of your cpp files, you did include
// > consoletaeftemplates.hpp properly and they would have compiled the actual
// > specialization from consoletaeftemplates.hpp into the corresponding obj
// > file for that cpp file. When the test DLL was linked, the linker picks one
// > of the multiple definitions available from the different obj files for
// > VerifyOutputTraits<COORD>. The linker happened to pick the one from the cpp
// > file where consoletaeftemplates.hpp was not included properly. Ive
// > encountered a similar situation before and it was baffling because the
// > compiled code was obviously doing different behavior than what the source
// > code said. This can happen when you violate the one-definition rule.
namespace WEX::TestExecution
{
template<>

View file

@ -0,0 +1,50 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- winrtTaefTemplates.hpp
Abstract:
- This module contains common TAEF templates for winrt-isms that don't otherwise
have them. This is very similar to consoleTaefTemplates, but this one presumes
that the winrt headers have already been included.
Author:
- Mike Griese 2021
--*/
#pragma once
// Thinking of adding a new VerifyOutputTraits for a new type? MAKE SURE that
// you include this header (or at least the relevant definition) before _every_
// Verify for that type.
//
// From a thread on the matter in 2018:
// > my best guess is that one of your cpp files uses a COORD in a Verify macro
// > without including consoletaeftemplates.hpp. This caused the
// > VerifyOutputTraits<COORD> symbol to be used with the default
// > implementation. In other of your cpp files, you did include
// > consoletaeftemplates.hpp properly and they would have compiled the actual
// > specialization from consoletaeftemplates.hpp into the corresponding obj
// > file for that cpp file. When the test DLL was linked, the linker picks one
// > of the multiple definitions available from the different obj files for
// > VerifyOutputTraits<COORD>. The linker happened to pick the one from the cpp
// > file where consoletaeftemplates.hpp was not included properly. Ive
// > encountered a similar situation before and it was baffling because the
// > compiled code was obviously doing different behavior than what the source
// > code said. This can happen when you violate the one-definition rule.
namespace WEX::TestExecution
{
template<>
class VerifyOutputTraits<winrt::hstring>
{
public:
static WEX::Common::NoThrowString ToString(const winrt::hstring& hstr)
{
return WEX::Common::NoThrowString().Format(L"%s", hstr.c_str());
}
};
}

View file

@ -38,7 +38,7 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid
return S_OK;
}
[[nodiscard]] HRESULT BgfxEngine::InvalidateCursor(const COORD* const /*pcoordCursor*/) noexcept
[[nodiscard]] HRESULT BgfxEngine::InvalidateCursor(const SMALL_RECT* const /*psrRegion*/) noexcept
{
return S_OK;
}

View file

@ -34,7 +34,7 @@ namespace Microsoft::Console::Render
// IRenderEngine Members
[[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;

View file

@ -41,6 +41,18 @@ HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noe
return S_FALSE;
}
HRESULT RenderEngineBase::ResetLineTransform() noexcept
{
return S_FALSE;
}
HRESULT RenderEngineBase::PrepareLineTransform(const LineRendition /*lineRendition*/,
const size_t /*targetRow*/,
const size_t /*viewportLeft*/) noexcept
{
return S_FALSE;
}
// Method Description:
// - By default, no one should need continuous redraw. It ruins performance
// in terms of CPU, memory, and battery life to just paint forever.

View file

@ -6,7 +6,7 @@
<RootNamespace>base</RootNamespace>
<ProjectName>RendererBase</ProjectName>
<TargetName>ConRenderBase</TargetName>
<ConfigurationType>StaticLibrary</ConfigurationType>
<ConfigurationType>StaticLibrary</ConfigurationType>
</PropertyGroup>
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<ItemGroup>

View file

@ -220,6 +220,18 @@ void Renderer::TriggerRedraw(const Viewport& region)
Viewport view = _viewport;
SMALL_RECT srUpdateRegion = region.ToExclusive();
// If the dirty region has double width lines, we need to double the size of
// the right margin to make sure all the affected cells are invalidated.
const auto& buffer = _pData->GetTextBuffer();
for (auto row = srUpdateRegion.Top; row < srUpdateRegion.Bottom; row++)
{
if (buffer.IsDoubleWidthLine(row))
{
srUpdateRegion.Right *= 2;
break;
}
}
if (view.TrimToViewport(&srUpdateRegion))
{
view.ConvertToOrigin(&srUpdateRegion);
@ -253,25 +265,34 @@ void Renderer::TriggerRedraw(const COORD* const pcoord)
// - <none>
void Renderer::TriggerRedrawCursor(const COORD* const pcoord)
{
Viewport view = _pData->GetViewport();
COORD updateCoord = *pcoord;
if (view.IsInBounds(updateCoord))
// We first need to make sure the cursor position is within the buffer,
// otherwise testing for a double width character can throw an exception.
const auto& buffer = _pData->GetTextBuffer();
if (buffer.GetSize().IsInBounds(*pcoord))
{
view.ConvertToOrigin(&updateCoord);
for (IRenderEngine* pEngine : _rgpEngines)
// We then calculate the region covered by the cursor. This requires
// converting the buffer coordinates to an equivalent range of screen
// cells for the cursor, taking line rendition into account.
const LineRendition lineRendition = buffer.GetLineRendition(pcoord->Y);
const SHORT cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1;
const SMALL_RECT cursorRect = { pcoord->X, pcoord->Y, pcoord->X + cursorWidth - 1, pcoord->Y };
Viewport cursorView = Viewport::FromInclusive(BufferToScreenLine(cursorRect, lineRendition));
// The region is clamped within the viewport boundaries and we only
// trigger a redraw if the region is not empty.
Viewport view = _pData->GetViewport();
cursorView = view.Clamp(cursorView);
if (cursorView.IsValid())
{
LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord));
// Double-wide cursors need to invalidate the right half as well.
if (_pData->IsCursorDoubleWidth())
const SMALL_RECT updateRect = view.ConvertToOrigin(cursorView).ToExclusive();
for (IRenderEngine* pEngine : _rgpEngines)
{
updateCoord.X++;
LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord));
LOG_IF_FAILED(pEngine->InvalidateCursor(&updateRect));
}
}
_NotifyPaintFrame();
_NotifyPaintFrame();
}
}
}
@ -630,6 +651,11 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
gsl::span<const til::rectangle> dirtyAreas;
LOG_IF_FAILED(pEngine->GetDirtyArea(dirtyAreas));
// This is to make sure any transforms are reset when this paint is finished.
auto resetLineTransform = wil::scope_exit([&]() {
LOG_IF_FAILED(pEngine->ResetLineTransform());
});
for (const auto& dirtyRect : dirtyAreas)
{
auto dirty = Viewport::FromInclusive(dirtyRect);
@ -654,14 +680,19 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
{
// Calculate the boundaries of a single line. This is from the left to right edge of the dirty
// area in width and exactly 1 tall.
const auto bufferLine = Viewport::FromDimensions({ redraw.Left(), row }, { redraw.Width(), 1 });
const auto screenLine = SMALL_RECT{ redraw.Left(), row, redraw.RightInclusive(), row };
// Convert the screen coordinates of the line to an equivalent
// range of buffer cells, taking line rendition into account.
const auto lineRendition = buffer.GetLineRendition(row);
const auto bufferLine = Viewport::FromInclusive(ScreenToBufferLine(screenLine, lineRendition));
// Find where on the screen we should place this line information. This requires us to re-map
// the buffer-based origin of the line back onto the screen-based origin of the line
// For example, the screen might say we need to paint 1,1 because it is dirty but the viewport is actually looking
// at 13,26 relative to the buffer.
// This means that we need 14,27 out of the backing buffer to fill in the 1,1 cell of the screen.
const auto screenLine = Viewport::Offset(bufferLine, -view.Origin());
// the buffer-based origin of the line back onto the screen-based origin of the line.
// For example, the screen might say we need to paint line 1 because it is dirty but the viewport
// is actually looking at line 26 relative to the buffer. This means that we need line 27 out
// of the backing buffer to fill in line 1 of the screen.
const auto screenPosition = bufferLine.Origin() - COORD{ 0, view.Top() };
// Retrieve the cell information iterator limited to just this line we want to redraw.
auto it = buffer.GetCellDataAt(bufferLine.Origin(), bufferLine);
@ -673,8 +704,11 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
const auto lineWrapped = (buffer.GetRowByOffset(bufferLine.Origin().Y).WasWrapForced()) &&
(bufferLine.RightExclusive() == buffer.GetSize().Width());
// Prepare the appropriate line transform for the current row and viewport offset.
LOG_IF_FAILED(pEngine->PrepareLineTransform(lineRendition, screenPosition.Y, view.Left()));
// Ask the helper to paint through this specific line.
_PaintBufferOutputHelper(pEngine, it, screenLine.Origin(), lineWrapped);
_PaintBufferOutputHelper(pEngine, it, screenPosition, lineWrapped);
}
}
}
@ -959,11 +993,25 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin
// bottom of the viewport, the space that's not quite a full line in
// height. Since we don't draw that text, we shouldn't draw the cursor
// there either.
Viewport view = _pData->GetViewport();
if (view.IsInBounds(coordCursor))
// The cursor is never rendered as double height, so we don't care about
// the exact line rendition - only whether it's double width or not.
const auto doubleWidth = _pData->GetTextBuffer().IsDoubleWidthLine(coordCursor.Y);
const auto lineRendition = doubleWidth ? LineRendition::DoubleWidth : LineRendition::SingleWidth;
// We need to convert the screen coordinates of the viewport to an
// equivalent range of buffer cells, taking line rendition into account.
const auto view = ScreenToBufferLine(_pData->GetViewport().ToInclusive(), lineRendition);
// Note that we allow the X coordinate to be outside the left border by 1 position,
// because the cursor could still be visible if the focused character is double width.
const auto xInRange = coordCursor.X >= view.Left - 1 && coordCursor.X <= view.Right;
const auto yInRange = coordCursor.Y >= view.Top && coordCursor.Y <= view.Bottom;
if (xInRange && yInRange)
{
// Adjust cursor to viewport
view.ConvertToOrigin(&coordCursor);
// Adjust cursor Y offset to viewport.
// The viewport X offset is saved in the options and handled with a transform.
coordCursor.Y -= view.Top;
COLORREF cursorColor = _pData->GetCursorColor();
bool useColor = cursorColor != INVALID_COLOR;
@ -971,6 +1019,8 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin
// Build up the cursor parameters including position, color, and drawing options
CursorOptions options;
options.coordCursor = coordCursor;
options.viewportLeft = _pData->GetViewport().Left();
options.lineRendition = lineRendition;
options.ulCursorHeightPercent = _pData->GetCursorHeight();
options.cursorPixelWidth = _pData->GetCursorPixelWidth();
options.fIsDoubleWidth = _pData->IsCursorDoubleWidth();
@ -1165,14 +1215,20 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine)
// - A vector of rectangles representing the regions to select, line by line.
std::vector<SMALL_RECT> Renderer::_GetSelectionRects() const
{
const auto& buffer = _pData->GetTextBuffer();
auto rects = _pData->GetSelectionRects();
// Adjust rectangles to viewport
Viewport view = _pData->GetViewport();
std::vector<SMALL_RECT> result;
for (auto& rect : rects)
for (auto rect : rects)
{
// Convert buffer offsets to the equivalent range of screen cells
// expected by callers, taking line rendition into account.
const auto lineRendition = buffer.GetLineRendition(rect.Top());
rect = Viewport::FromInclusive(BufferToScreenLine(rect.ToInclusive(), lineRendition));
auto sr = view.ConvertToOrigin(rect).ToInclusive();
// hopefully temporary, we should be receiving the right selection sizes without correction.

View file

@ -17,43 +17,21 @@ using namespace Microsoft::Console::Render;
// Routine Description:
// - Creates a CustomTextLayout object for calculating which glyphs should be placed and where
// Arguments:
// - factory - DirectWrite factory reference in case we need other DirectWrite objects for our layout
// - analyzer - DirectWrite text analyzer from the factory that has been cached at a level above this layout (expensive to create)
// - format - The DirectWrite format object representing the size and other text properties to be applied (by default) to a layout
// - formatItalic - The italic variant of the format object representing the size and other text properties for italic text
// - font - The DirectWrite font face to use while calculating layout (by default, will fallback if necessary)
// - fontItalic - The italic variant of the font face to use while calculating layout for italic text
// - width - The count of pixels available per column (the expected pixel width of every column)
// - boxEffect - Box drawing scaling effects that are cached for the base font across layouts.
CustomTextLayout::CustomTextLayout(gsl::not_null<IDWriteFactory1*> const factory,
gsl::not_null<IDWriteTextAnalyzer1*> const analyzer,
gsl::not_null<IDWriteTextFormat*> const format,
gsl::not_null<IDWriteTextFormat*> const formatItalic,
gsl::not_null<IDWriteFontFace1*> const font,
gsl::not_null<IDWriteFontFace1*> const fontItalic,
size_t const width,
IBoxDrawingEffect* const boxEffect) :
_factory{ factory.get() },
_analyzer{ analyzer.get() },
_format{ format.get() },
_formatItalic{ formatItalic.get() },
_formatInUse{ format.get() },
_font{ font.get() },
_fontItalic{ fontItalic.get() },
_fontInUse{ font.get() },
_boxDrawingEffect{ boxEffect },
_localeName{},
// - dxFontRenderData - The DirectWrite font render data for our layout
CustomTextLayout::CustomTextLayout(gsl::not_null<DxFontRenderData*> const fontRenderData) :
_fontRenderData{ fontRenderData },
_formatInUse{ fontRenderData->DefaultTextFormat().Get() },
_fontInUse{ fontRenderData->DefaultFontFace().Get() },
_numberSubstitution{},
_readingDirection{ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT },
_runs{},
_breakpoints{},
_runIndex{ 0 },
_width{ width },
_width{ gsl::narrow_cast<size_t>(fontRenderData->GlyphCell().width()) },
_isEntireTextSimple{ false }
{
// Fetch the locale name out once now from the format
_localeName.resize(gsl::narrow_cast<size_t>(format->GetLocaleNameLength()) + 1); // +1 for null
THROW_IF_FAILED(format->GetLocaleName(_localeName.data(), gsl::narrow<UINT32>(_localeName.size())));
_localeName.resize(gsl::narrow_cast<size_t>(fontRenderData->DefaultTextFormat()->GetLocaleNameLength()) + 1); // +1 for null
THROW_IF_FAILED(fontRenderData->DefaultTextFormat()->GetLocaleName(_localeName.data(), gsl::narrow<UINT32>(_localeName.size())));
}
//Routine Description:
@ -122,8 +100,8 @@ CATCH_RETURN()
RETURN_HR_IF_NULL(E_INVALIDARG, columns);
*columns = 0;
_formatInUse = _format.Get();
_fontInUse = _font.Get();
_formatInUse = _fontRenderData->DefaultTextFormat().Get();
_fontInUse = _fontRenderData->DefaultFontFace().Get();
RETURN_IF_FAILED(_AnalyzeTextComplexity());
RETURN_IF_FAILED(_AnalyzeRuns());
@ -157,8 +135,8 @@ CATCH_RETURN()
FLOAT originY) noexcept
{
const auto drawingContext = static_cast<const DrawingContext*>(clientDrawingContext);
_formatInUse = drawingContext->useItalicFont ? _formatItalic.Get() : _format.Get();
_fontInUse = drawingContext->useItalicFont ? _fontItalic.Get() : _font.Get();
_formatInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicTextFormat().Get() : _fontRenderData->DefaultTextFormat().Get();
_fontInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicFontFace().Get() : _fontRenderData->DefaultFontFace().Get();
RETURN_IF_FAILED(_AnalyzeTextComplexity());
RETURN_IF_FAILED(_AnalyzeRuns());
@ -196,7 +174,7 @@ CATCH_RETURN()
_glyphIndices.resize(textLength);
const HRESULT hr = _analyzer->GetTextComplexity(
const HRESULT hr = _fontRenderData->Analyzer()->GetTextComplexity(
_text.c_str(),
textLength,
_fontInUse,
@ -243,10 +221,10 @@ CATCH_RETURN()
if (!_isEntireTextSimple)
{
// Call each of the analyzers in sequence, recording their results.
RETURN_IF_FAILED(_analyzer->AnalyzeLineBreakpoints(this, 0, textLength, this));
RETURN_IF_FAILED(_analyzer->AnalyzeBidi(this, 0, textLength, this));
RETURN_IF_FAILED(_analyzer->AnalyzeScript(this, 0, textLength, this));
RETURN_IF_FAILED(_analyzer->AnalyzeNumberSubstitution(this, 0, textLength, this));
RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeLineBreakpoints(this, 0, textLength, this));
RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeBidi(this, 0, textLength, this));
RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeScript(this, 0, textLength, this));
RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeNumberSubstitution(this, 0, textLength, this));
// Perform our custom font fallback analyzer that mimics the pattern of the real analyzers.
RETURN_IF_FAILED(_AnalyzeFontFallback(this, 0, textLength));
}
@ -407,7 +385,7 @@ CATCH_RETURN()
HRESULT hr = S_OK;
do
{
hr = _analyzer->GetGlyphs(
hr = _fontRenderData->Analyzer()->GetGlyphs(
&_text.at(textStart),
textLength,
run.fontFace.Get(),
@ -452,7 +430,7 @@ CATCH_RETURN()
const auto fontSizeFormat = _formatInUse->GetFontSize();
const auto fontSize = fontSizeFormat * run.fontScale;
hr = _analyzer->GetGlyphPlacements(
hr = _fontRenderData->Analyzer()->GetGlyphPlacements(
&_text.at(textStart),
&_glyphClusters.at(textStart),
&textProps.at(0),
@ -1265,9 +1243,7 @@ CATCH_RETURN();
if (!fallback)
{
::Microsoft::WRL::ComPtr<IDWriteFactory2> factory2;
RETURN_IF_FAILED(_factory.As(&factory2));
factory2->GetSystemFontFallback(&fallback);
fallback = _fontRenderData->SystemFontFallback();
}
// Walk through and analyze the entire string
@ -1467,14 +1443,14 @@ try
{
auto& run = _FetchNextRun(textLength);
if (run.fontFace == _font)
if (run.fontFace == _fontRenderData->DefaultFontFace())
{
run.drawingEffect = _boxDrawingEffect;
run.drawingEffect = _fontRenderData->DefaultBoxDrawingEffect();
}
else
{
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> eff;
RETURN_IF_FAILED(s_CalculateBoxEffect(_formatInUse, _width, run.fontFace.Get(), run.fontScale, &eff));
RETURN_IF_FAILED(DxFontRenderData::s_CalculateBoxEffect(_formatInUse, _width, run.fontFace.Get(), run.fontScale, &eff));
// store data in the run
run.drawingEffect = std::move(eff);
@ -1485,247 +1461,6 @@ try
}
CATCH_RETURN();
// Routine Description:
// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible.
// Arguments:
// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font.
// - widthPixels - The pixel width of the available cell.
// - face - The font face that is currently being used, may differ from the base font from the layout.
// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling.
// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required.
// Return Value:
// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors.
[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept
try
{
// Check for bad in parameters.
RETURN_HR_IF(E_INVALIDARG, !format);
RETURN_HR_IF(E_INVALIDARG, !face);
// Check the out parameter and fill it up with null.
RETURN_HR_IF(E_INVALIDARG, !effect);
*effect = nullptr;
// The format is based around the main font that was specified by the user.
// We need to know its size as well as the final spacing that was calculated around
// it when it was first selected to get an idea of how large the bounding box is.
const auto fontSize = format->GetFontSize();
DWRITE_LINE_SPACING_METHOD spacingMethod;
float lineSpacing; // total height of the cells
float baseline; // vertical position counted down from the top where the characters "sit"
RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline));
const float ascentPixels = baseline;
const float descentPixels = lineSpacing - baseline;
// We need this for the designUnitsPerEm which will be required to move back and forth between
// Design Units and Pixels. I'll elaborate below.
DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
// If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different
// than the font size used for the original format (IDWriteTextFormat).
const auto scaledFontSize = fontScale * fontSize;
// This is Unicode FULL BLOCK U+2588.
// We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis
// in knowing exactly where to touch every single edge.
// We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe
// inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent.
const UINT32 blockCodepoint = L'\x2588';
// Get the index of the block out of the font.
UINT16 glyphIndex;
RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex));
// If it was 0, it wasn't found in the font. We're going to try again with
// Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching
// all the edges of the possible rectangle, much like a full block should.
if (glyphIndex == 0)
{
const UINT32 alternateCp = L'\x253C';
RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex));
}
// If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions.
// So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't
// stop the rendering pipeline.
RETURN_HR_IF(S_FALSE, glyphIndex == 0);
// Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing
// glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet.
DWRITE_GLYPH_METRICS boxMetrics = { 0 };
RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics));
// NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic
// way of describing proportions.
// Converting back and forth between real pixels and design units is possible using
// any font's specific fontSize and the designUnitsPerEm FONT_METRIC value.
//
// Here's what to know about the boxMetrics:
//
//
//
// topLeft --> +--------------------------------+ ---
// | ^ | |
// | | topSide | |
// | | Bearing | |
// | v | |
// | +-----------------+ | |
// | | | | |
// | | | | | a
// | | | | | d
// | | | | | v
// +<---->+ | | | a
// | | | | | n
// | left | | | | c
// | Side | | | | e
// | Bea- | | | | H
// | ring | | right | | e
// vertical | | | Side | | i
// OriginY --> x | | Bea- | | g
// | | | ring | | h
// | | | | | t
// | | +<----->+ |
// | +-----------------+ | |
// | ^ | |
// | bottomSide | | |
// | Bearing | | |
// | v | |
// +--------------------------------+ ---
//
//
// | |
// +--------------------------------+
// | advanceWidth |
//
//
// NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box
// as defined by the advanceHeight/width.
// See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics
// The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens.
const float defaultBoxVerticalScaleFactor = 1.0f;
float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor;
const float defaultBoxVerticalTranslation = 0.0f;
float boxVerticalTranslation = defaultBoxVerticalTranslation;
{
// First, find the dimensions of the glyph representing our fully filled box.
// Ascent is how far up from the baseline we'll draw.
// verticalOriginY is the measure from the topLeft corner of the bounding box down to where
// the glyph's version of the baseline is.
// topSideBearing is how much "gap space" is left between that topLeft and where the glyph
// starts drawing. Subtract the gap space to find how far is drawn upward from baseline.
const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing;
// Descent is how far down from the baseline we'll draw.
// advanceHeight is the total height of the drawn bounding box.
// verticalOriginY is how much was given to the ascent, so subtract that out.
// What remains is then the descent value. Remove the
// bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline.
const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing;
// The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below.
const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits;
// Second, find the dimensions of the cell we're going to attempt to fit within.
// We know about the exact ascent/descent units in pixels as calculated when we chose a font and
// adjusted the ascent/descent for a nice perfect baseline and integer total height.
// All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above.
// Use the formula: Pixels * Design Units Per Em / Font Size = Design Units
const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits;
// OK, now do a few checks. If the drawn box touches the top and bottom of the cell
// and the box is overall tall enough, then we'll not bother adjusting.
// We will presume the font author has set things as they wish them to be.
const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits;
const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits;
const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits;
// If not...
if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell))
{
// Find a scaling factor that will make the total height drawn of this box
// perfectly fit the same number of design units as the cell.
// Since scale factor is a multiplier, it doesn't matter that this is design units.
// The fraction between the two heights in pixels should be exactly the same
// (which is what will matter when we go to actually render it... the pixels that is.)
// Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale.
boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f);
// The box as scaled might be hanging over the top or bottom of the cell (or both).
// We find out the amount of overhang/underhang on both the top and the bottom.
const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits;
const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits;
// This took a bit of time and effort and it's difficult to put into words, but here goes.
// We want the average of the two magnitudes to find out how much to "take" from one and "give"
// to the other such that both are equal. We presume the glyphs are designed to be drawn
// centered in their box vertically to look good.
// The ordering around subtraction is required to ensure that the direction is correct with a negative
// translation moving up (taking excess descent and adding to ascent) and positive is the opposite.
const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2;
// The translation is just a raw movement of pixels up or down. Since we were working in Design Units,
// we need to run the opposite algorithm shown above to go from Design Units to Pixels.
boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// The horizontal adjustments follow the exact same logic as the vertical ones.
const float defaultBoxHorizontalScaleFactor = 1.0f;
float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor;
const float defaultBoxHorizontalTranslation = 0.0f;
float boxHorizontalTranslation = defaultBoxHorizontalTranslation;
{
// This is the only difference. We don't have a horizontalOriginX from the metrics.
// However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says
// the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin.
// So we'll use that as the "center" and apply it the role that verticalOriginY had above.
const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2;
const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing;
const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits;
const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits;
const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellLeftDesignUnits = cellWidthDesignUnits / 2;
const auto cellRightDesignUnits = cellLeftDesignUnits;
const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits;
const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits;
const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits;
if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell))
{
boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f);
const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits;
const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits;
const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2;
boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// If we set anything, make a drawing effect. Otherwise, there isn't one.
if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor ||
defaultBoxVerticalTranslation != boxVerticalTranslation ||
defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor ||
defaultBoxHorizontalTranslation != boxHorizontalTranslation)
{
// OK, make the object that will represent our effect, stuff the metrics into it, and return it.
RETURN_IF_FAILED(WRL::MakeAndInitialize<BoxDrawingEffect>(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation));
}
return S_OK;
}
CATCH_RETURN()
#pragma endregion
#pragma region internal Run manipulation functions for storing information from sink callbacks

View file

@ -11,6 +11,7 @@
#include <wrl/implements.h>
#include "BoxDrawingEffect.h"
#include "DxFontRenderData.h"
#include "../inc/Cluster.hpp"
namespace Microsoft::Console::Render
@ -20,14 +21,7 @@ namespace Microsoft::Console::Render
public:
// Based on the Windows 7 SDK sample at https://github.com/pauldotknopf/WindowsSDK7-Samples/tree/master/multimedia/DirectWrite/CustomLayout
CustomTextLayout(gsl::not_null<IDWriteFactory1*> const factory,
gsl::not_null<IDWriteTextAnalyzer1*> const analyzer,
gsl::not_null<IDWriteTextFormat*> const normalFormat,
gsl::not_null<IDWriteTextFormat*> const italicFormat,
gsl::not_null<IDWriteFontFace1*> const normalFont,
gsl::not_null<IDWriteFontFace1*> const italicFont,
size_t const width,
IBoxDrawingEffect* const boxEffect);
CustomTextLayout(gsl::not_null<DxFontRenderData*> const fontRenderData);
[[nodiscard]] HRESULT STDMETHODCALLTYPE AppendClusters(const gsl::span<const ::Microsoft::Console::Render::Cluster> clusters);
@ -71,8 +65,6 @@ namespace Microsoft::Console::Render
UINT32 textLength,
_In_ IDWriteNumberSubstitution* numberSubstitution) override;
[[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept;
protected:
// A single contiguous run of characters containing the same analysis results.
struct Run
@ -157,24 +149,15 @@ namespace Microsoft::Console::Render
[[nodiscard]] static constexpr UINT32 _EstimateGlyphCount(const UINT32 textLength) noexcept;
private:
const ::Microsoft::WRL::ComPtr<IDWriteFactory1> _factory;
// DirectWrite analyzer
const ::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _analyzer;
// DirectWrite font render data
DxFontRenderData* _fontRenderData;
// DirectWrite text formats
const ::Microsoft::WRL::ComPtr<IDWriteTextFormat> _format;
const ::Microsoft::WRL::ComPtr<IDWriteTextFormat> _formatItalic;
IDWriteTextFormat* _formatInUse;
// DirectWrite font faces
const ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _font;
const ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _fontItalic;
IDWriteFontFace1* _fontInUse;
// Box drawing effect
const ::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
// The text we're analyzing and processing into a layout
std::wstring _text;
std::vector<UINT16> _textClusterColumns;

View file

@ -0,0 +1,755 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "DxFontRenderData.h"
static constexpr float POINTS_PER_INCH = 72.0f;
static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" };
static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us";
using namespace Microsoft::Console::Render;
DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory) noexcept :
_dwriteFactory(dwriteFactory),
_glyphCell{},
_lineMetrics({}),
_boxDrawingEffect{}
{
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> DxFontRenderData::Analyzer() noexcept
{
return _dwriteTextAnalyzer;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFallback> DxFontRenderData::SystemFontFallback()
{
if (!_systemFontFallback)
{
::Microsoft::WRL::ComPtr<IDWriteFactory2> factory2;
THROW_IF_FAILED(_dwriteFactory.As(&factory2));
factory2->GetSystemFontFallback(&_systemFontFallback);
}
return _systemFontFallback;
}
[[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept
{
return _glyphCell;
}
[[nodiscard]] DxFontRenderData::LineMetrics DxFontRenderData::GetLineMetrics() noexcept
{
return _lineMetrics;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::DefaultTextFormat() noexcept
{
return _dwriteTextFormat;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::DefaultFontFace() noexcept
{
return _dwriteFontFace;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DxFontRenderData::DefaultBoxDrawingEffect() noexcept
{
return _boxDrawingEffect;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::ItalicTextFormat() noexcept
{
return _dwriteTextFormatItalic;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::ItalicFontFace() noexcept
{
return _dwriteFontFaceItalic;
}
// Routine Description:
// - Updates the font used for drawing
// Arguments:
// - desired - Information specifying the font that is requested
// - actual - Filled with the nearest font actually chosen for drawing
// - dpi - The DPI of the screen
// Return Value:
// - S_OK or relevant DirectX error
[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi) noexcept
{
try
{
_userLocaleName.clear();
std::wstring fontName(desired.GetFaceName());
DWRITE_FONT_WEIGHT weight = static_cast<DWRITE_FONT_WEIGHT>(desired.GetWeight());
DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL;
DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL;
std::wstring localeName = _GetUserLocaleName();
// _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font,
// but we should use the system's locale to render the text.
std::wstring fontLocaleName = localeName;
const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName);
DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
const UINT32 spaceCodePoint = L'M';
UINT16 spaceGlyphIndex;
THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex));
INT32 advanceInDesignUnits;
THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits));
DWRITE_GLYPH_METRICS spaceMetrics = { 0 };
THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics));
// The math here is actually:
// Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor.
// - DPI = dots per inch
// - PPI = points per inch or "points" as usually seen when choosing a font size
// - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI.
// - The Points to Pixels factor is based on the typography definition of 72 points per inch.
// As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch
// to get a factor of 1 and 1/3.
// This turns into something like:
// - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%)
// - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%)
// - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%)
float heightDesired = static_cast<float>(desired.GetEngineSize().Y) * static_cast<float>(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH;
// The advance is the number of pixels left-to-right (X dimension) for the given font.
// We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement.
// Now we play trickery with the font size. Scale by the DPI to get the height we expect.
heightDesired *= (static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI));
const float widthAdvance = static_cast<float>(advanceInDesignUnits) / fontMetrics.designUnitsPerEm;
// Use the real pixel height desired by the "em" factor for the width to get the number of pixels
// we will need per character in width. This will almost certainly result in fractional X-dimension pixels.
const float widthApprox = heightDesired * widthAdvance;
// Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel.
const float widthExact = round(widthApprox);
// Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional
// height in pixels of each character. It's easier for us to pad out height and align vertically
// than it is horizontally.
const auto fontSize = widthExact / widthAdvance;
// Now figure out the basic properties of the character height which include ascent and descent
// for this specific font size.
const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm;
const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm;
// Get the gap.
const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm;
const float halfGap = gap / 2;
// We're going to build a line spacing object here to track all of this data in our format.
DWRITE_LINE_SPACING lineSpacing = {};
lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM;
// We need to make sure the baseline falls on a round pixel (not a fractional pixel).
// If the baseline is fractional, the text appears blurry, especially at small scales.
// Since we also need to make sure the bounding box as a whole is round pixels
// (because the entire console system maths in full cell units),
// we're just going to ceiling up the ascent and descent to make a full pixel amount
// and set the baseline to the full round pixel ascent value.
//
// For reference, for the letters "ag":
// ...
// gggggg bottom of previous line
//
// ----------------- <===========================================|
// | topSideBearing | 1/2 lineGap |
// aaaaaa ggggggg <-------------------------|-------------| |
// a g g | | |
// aaaaa ggggg |<-ascent | |
// a a g | | |---- lineHeight
// aaaaa a gggggg <----baseline, verticalOriginY----------|---|
// g g |<-descent | |
// gggggg <-------------------------|-------------| |
// | bottomSideBearing | 1/2 lineGap |
// ----------------- <===========================================|
//
// aaaaaa ggggggg top of next line
// ...
//
// Also note...
// We're going to add half the line gap to the ascent and half the line gap to the descent
// to ensure that the spacing is balanced vertically.
// Generally speaking, the line gap is added to the ascent by DirectWrite itself for
// horizontally drawn text which can place the baseline and glyphs "lower" in the drawing
// box than would be desired for proper alignment of things like line and box characters
// which will try to sit centered in the area and touch perfectly with their neighbors.
const auto fullPixelAscent = ceil(ascent + halfGap);
const auto fullPixelDescent = ceil(descent + halfGap);
lineSpacing.height = fullPixelAscent + fullPixelDescent;
lineSpacing.baseline = fullPixelAscent;
// According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage)
// Setting "ENABLED" means we've included the line gapping in the spacing numbers given.
lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED;
// Create the font with the fractional pixel height size.
// It should have an integer pixel width by our math above.
// Then below, apply the line spacing to the format to position the floating point pixel height characters
// into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out.
Microsoft::WRL::ComPtr<IDWriteTextFormat> format;
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontName.data(),
nullptr,
weight,
style,
stretch,
fontSize,
localeName.data(),
&format));
THROW_IF_FAILED(format.As(&_dwriteTextFormat));
// We also need to create an italic variant of the font face and text
// format, based on the same parameters, but using an italic style.
std::wstring fontNameItalic = fontName;
DWRITE_FONT_WEIGHT weightItalic = weight;
DWRITE_FONT_STYLE styleItalic = DWRITE_FONT_STYLE_ITALIC;
DWRITE_FONT_STRETCH stretchItalic = stretch;
const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName);
Microsoft::WRL::ComPtr<IDWriteTextFormat> formatItalic;
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(),
nullptr,
weightItalic,
styleItalic,
stretchItalic,
fontSize,
localeName.data(),
&formatItalic));
THROW_IF_FAILED(formatItalic.As(&_dwriteTextFormatItalic));
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer> analyzer;
THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer));
_dwriteFontFace = face;
_dwriteFontFaceItalic = faceItalic;
THROW_IF_FAILED(_dwriteTextFormat->SetLineSpacing(lineSpacing.method, lineSpacing.height, lineSpacing.baseline));
THROW_IF_FAILED(_dwriteTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR));
THROW_IF_FAILED(_dwriteTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
// The scaled size needs to represent the pixel box that each character will fit within for the purposes
// of hit testing math and other such multiplication/division.
COORD coordSize = { 0 };
coordSize.X = gsl::narrow<SHORT>(widthExact);
coordSize.Y = gsl::narrow_cast<SHORT>(lineSpacing.height);
// Unscaled is for the purposes of re-communicating this font back to the renderer again later.
// As such, we need to give the same original size parameter back here without padding
// or rounding or scaling manipulation.
const COORD unscaled = desired.GetEngineSize();
const COORD scaled = coordSize;
actual.SetFromEngine(fontName,
desired.GetFamily(),
_dwriteTextFormat->GetFontWeight(),
false,
scaled,
unscaled);
LineMetrics lineMetrics;
// There is no font metric for the grid line width, so we use a small
// multiple of the font size, which typically rounds to a pixel.
lineMetrics.gridlineWidth = std::round(fontSize * 0.025f);
// All other line metrics are in design units, so to get a pixel value,
// we scale by the font size divided by the design-units-per-em.
const auto scale = fontSize / fontMetrics.designUnitsPerEm;
lineMetrics.underlineOffset = std::round(fontMetrics.underlinePosition * scale);
lineMetrics.underlineWidth = std::round(fontMetrics.underlineThickness * scale);
lineMetrics.strikethroughOffset = std::round(fontMetrics.strikethroughPosition * scale);
lineMetrics.strikethroughWidth = std::round(fontMetrics.strikethroughThickness * scale);
// We always want the lines to be visible, so if a stroke width ends up
// at zero after rounding, we need to make it at least 1 pixel.
lineMetrics.gridlineWidth = std::max(lineMetrics.gridlineWidth, 1.0f);
lineMetrics.underlineWidth = std::max(lineMetrics.underlineWidth, 1.0f);
lineMetrics.strikethroughWidth = std::max(lineMetrics.strikethroughWidth, 1.0f);
// Offsets are relative to the base line of the font, so we subtract
// from the ascent to get an offset relative to the top of the cell.
lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset;
lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset;
// For double underlines we need a second offset, just below the first,
// but with a bit of a gap (about double the grid line width).
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset +
lineMetrics.underlineWidth +
std::round(fontSize * 0.05f);
// However, we don't want the underline to extend past the bottom of the
// cell, so we clamp the offset to fit just inside.
const auto maxUnderlineOffset = lineSpacing.height - lineMetrics.underlineWidth;
lineMetrics.underlineOffset2 = std::min(lineMetrics.underlineOffset2, maxUnderlineOffset);
// But if the resulting gap isn't big enough even to register as a thicker
// line, it's better to place the second line slightly above the first.
if (lineMetrics.underlineOffset2 < lineMetrics.underlineOffset + lineMetrics.gridlineWidth)
{
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth;
}
// We also add half the stroke width to the offsets, since the line
// coordinates designate the center of the line.
lineMetrics.underlineOffset += lineMetrics.underlineWidth / 2.0f;
lineMetrics.underlineOffset2 += lineMetrics.underlineWidth / 2.0f;
lineMetrics.strikethroughOffset += lineMetrics.strikethroughWidth / 2.0f;
_lineMetrics = lineMetrics;
_glyphCell = actual.GetSize();
// Calculate and cache the box effect for the base font. Scale is 1.0f because the base font is exactly the scale we want already.
RETURN_IF_FAILED(s_CalculateBoxEffect(DefaultTextFormat().Get(), _glyphCell.width(), DefaultFontFace().Get(), 1.0f, &_boxDrawingEffect));
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible.
// Arguments:
// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font.
// - widthPixels - The pixel width of the available cell.
// - face - The font face that is currently being used, may differ from the base font from the layout.
// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling.
// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required.
// Return Value:
// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors.
[[nodiscard]] HRESULT STDMETHODCALLTYPE DxFontRenderData::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept
try
{
// Check for bad in parameters.
RETURN_HR_IF(E_INVALIDARG, !format);
RETURN_HR_IF(E_INVALIDARG, !face);
// Check the out parameter and fill it up with null.
RETURN_HR_IF(E_INVALIDARG, !effect);
*effect = nullptr;
// The format is based around the main font that was specified by the user.
// We need to know its size as well as the final spacing that was calculated around
// it when it was first selected to get an idea of how large the bounding box is.
const auto fontSize = format->GetFontSize();
DWRITE_LINE_SPACING_METHOD spacingMethod;
float lineSpacing; // total height of the cells
float baseline; // vertical position counted down from the top where the characters "sit"
RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline));
const float ascentPixels = baseline;
const float descentPixels = lineSpacing - baseline;
// We need this for the designUnitsPerEm which will be required to move back and forth between
// Design Units and Pixels. I'll elaborate below.
DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
// If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different
// than the font size used for the original format (IDWriteTextFormat).
const auto scaledFontSize = fontScale * fontSize;
// This is Unicode FULL BLOCK U+2588.
// We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis
// in knowing exactly where to touch every single edge.
// We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe
// inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent.
const UINT32 blockCodepoint = L'\x2588';
// Get the index of the block out of the font.
UINT16 glyphIndex;
RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex));
// If it was 0, it wasn't found in the font. We're going to try again with
// Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching
// all the edges of the possible rectangle, much like a full block should.
if (glyphIndex == 0)
{
const UINT32 alternateCp = L'\x253C';
RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex));
}
// If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions.
// So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't
// stop the rendering pipeline.
RETURN_HR_IF(S_FALSE, glyphIndex == 0);
// Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing
// glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet.
DWRITE_GLYPH_METRICS boxMetrics = { 0 };
RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics));
// NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic
// way of describing proportions.
// Converting back and forth between real pixels and design units is possible using
// any font's specific fontSize and the designUnitsPerEm FONT_METRIC value.
//
// Here's what to know about the boxMetrics:
//
//
//
// topLeft --> +--------------------------------+ ---
// | ^ | |
// | | topSide | |
// | | Bearing | |
// | v | |
// | +-----------------+ | |
// | | | | |
// | | | | | a
// | | | | | d
// | | | | | v
// +<---->+ | | | a
// | | | | | n
// | left | | | | c
// | Side | | | | e
// | Bea- | | | | H
// | ring | | right | | e
// vertical | | | Side | | i
// OriginY --> x | | Bea- | | g
// | | | ring | | h
// | | | | | t
// | | +<----->+ |
// | +-----------------+ | |
// | ^ | |
// | bottomSide | | |
// | Bearing | | |
// | v | |
// +--------------------------------+ ---
//
//
// | |
// +--------------------------------+
// | advanceWidth |
//
//
// NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box
// as defined by the advanceHeight/width.
// See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics
// The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens.
const float defaultBoxVerticalScaleFactor = 1.0f;
float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor;
const float defaultBoxVerticalTranslation = 0.0f;
float boxVerticalTranslation = defaultBoxVerticalTranslation;
{
// First, find the dimensions of the glyph representing our fully filled box.
// Ascent is how far up from the baseline we'll draw.
// verticalOriginY is the measure from the topLeft corner of the bounding box down to where
// the glyph's version of the baseline is.
// topSideBearing is how much "gap space" is left between that topLeft and where the glyph
// starts drawing. Subtract the gap space to find how far is drawn upward from baseline.
const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing;
// Descent is how far down from the baseline we'll draw.
// advanceHeight is the total height of the drawn bounding box.
// verticalOriginY is how much was given to the ascent, so subtract that out.
// What remains is then the descent value. Remove the
// bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline.
const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing;
// The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below.
const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits;
// Second, find the dimensions of the cell we're going to attempt to fit within.
// We know about the exact ascent/descent units in pixels as calculated when we chose a font and
// adjusted the ascent/descent for a nice perfect baseline and integer total height.
// All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above.
// Use the formula: Pixels * Design Units Per Em / Font Size = Design Units
const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits;
// OK, now do a few checks. If the drawn box touches the top and bottom of the cell
// and the box is overall tall enough, then we'll not bother adjusting.
// We will presume the font author has set things as they wish them to be.
const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits;
const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits;
const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits;
// If not...
if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell))
{
// Find a scaling factor that will make the total height drawn of this box
// perfectly fit the same number of design units as the cell.
// Since scale factor is a multiplier, it doesn't matter that this is design units.
// The fraction between the two heights in pixels should be exactly the same
// (which is what will matter when we go to actually render it... the pixels that is.)
// Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale.
boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f);
// The box as scaled might be hanging over the top or bottom of the cell (or both).
// We find out the amount of overhang/underhang on both the top and the bottom.
const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits;
const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits;
// This took a bit of time and effort and it's difficult to put into words, but here goes.
// We want the average of the two magnitudes to find out how much to "take" from one and "give"
// to the other such that both are equal. We presume the glyphs are designed to be drawn
// centered in their box vertically to look good.
// The ordering around subtraction is required to ensure that the direction is correct with a negative
// translation moving up (taking excess descent and adding to ascent) and positive is the opposite.
const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2;
// The translation is just a raw movement of pixels up or down. Since we were working in Design Units,
// we need to run the opposite algorithm shown above to go from Design Units to Pixels.
boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// The horizontal adjustments follow the exact same logic as the vertical ones.
const float defaultBoxHorizontalScaleFactor = 1.0f;
float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor;
const float defaultBoxHorizontalTranslation = 0.0f;
float boxHorizontalTranslation = defaultBoxHorizontalTranslation;
{
// This is the only difference. We don't have a horizontalOriginX from the metrics.
// However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says
// the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin.
// So we'll use that as the "center" and apply it the role that verticalOriginY had above.
const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2;
const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing;
const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits;
const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits;
const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellLeftDesignUnits = cellWidthDesignUnits / 2;
const auto cellRightDesignUnits = cellLeftDesignUnits;
const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits;
const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits;
const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits;
if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell))
{
boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f);
const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits;
const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits;
const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2;
boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// If we set anything, make a drawing effect. Otherwise, there isn't one.
if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor ||
defaultBoxVerticalTranslation != boxVerticalTranslation ||
defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor ||
defaultBoxHorizontalTranslation != boxHorizontalTranslation)
{
// OK, make the object that will represent our effect, stuff the metrics into it, and return it.
RETURN_IF_FAILED(WRL::MakeAndInitialize<BoxDrawingEffect>(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation));
}
return S_OK;
}
CATCH_RETURN()
// Routine Description:
// - Attempts to locate the font given, but then begins falling back if we cannot find it.
// - We'll try to fall back to Consolas with the given weight/stretch/style first,
// then try Consolas again with normal weight/stretch/style,
// and if nothing works, then we'll throw an error.
// Arguments:
// - familyName - The font name we should be looking for
// - weight - The weight (bold, light, etc.)
// - stretch - The stretch of the font is the spacing between each letter
// - style - Normal, italic, etc.
// Return Value:
// - Smart pointer holding interface reference for queryable font data.
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::_ResolveFontFaceWithFallback(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const
{
auto face = _FindFontFace(familyName, weight, stretch, style, localeName);
if (!face)
{
for (const auto fallbackFace : FALLBACK_FONT_FACES)
{
familyName = fallbackFace;
face = _FindFontFace(familyName, weight, stretch, style, localeName);
if (face)
{
break;
}
familyName = fallbackFace;
weight = DWRITE_FONT_WEIGHT_NORMAL;
stretch = DWRITE_FONT_STRETCH_NORMAL;
style = DWRITE_FONT_STYLE_NORMAL;
face = _FindFontFace(familyName, weight, stretch, style, localeName);
if (face)
{
break;
}
}
}
THROW_HR_IF_NULL(E_FAIL, face);
return face;
}
// Routine Description:
// - Locates a suitable font face from the given information
// Arguments:
// - familyName - The font name we should be looking for
// - weight - The weight (bold, light, etc.)
// - stretch - The stretch of the font is the spacing between each letter
// - style - Normal, italic, etc.
// Return Value:
// - Smart pointer holding interface reference for queryable font data.
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::_FindFontFace(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const
{
Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
Microsoft::WRL::ComPtr<IDWriteFontCollection> fontCollection;
THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false));
UINT32 familyIndex;
BOOL familyExists;
THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists));
if (familyExists)
{
Microsoft::WRL::ComPtr<IDWriteFontFamily> fontFamily;
THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily));
Microsoft::WRL::ComPtr<IDWriteFont> font;
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font));
Microsoft::WRL::ComPtr<IDWriteFontFace> fontFace0;
THROW_IF_FAILED(font->CreateFontFace(&fontFace0));
THROW_IF_FAILED(fontFace0.As(&fontFace));
// Retrieve metrics in case the font we created was different than what was requested.
weight = font->GetWeight();
stretch = font->GetStretch();
style = font->GetStyle();
// Dig the family name out at the end to return it.
familyName = _GetFontFamilyName(fontFamily.Get(), localeName);
}
return fontFace;
}
// Routine Description:
// - Retrieves the font family name out of the given object in the given locale.
// - If we can't find a valid name for the given locale, we'll fallback and report it back.
// Arguments:
// - fontFamily - DirectWrite font family object
// - localeName - The locale in which the name should be retrieved.
// - If fallback occurred, this is updated to what we retrieved instead.
// Return Value:
// - Localized string name of the font family
[[nodiscard]] std::wstring DxFontRenderData::_GetFontFamilyName(gsl::not_null<IDWriteFontFamily*> const fontFamily,
std::wstring& localeName) const
{
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
Microsoft::WRL::ComPtr<IDWriteLocalizedStrings> familyNames;
THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames));
// First we have to find the right family name for the locale. We're going to bias toward what the caller
// requested, but fallback if we need to and reply with the locale we ended up choosing.
UINT32 index = 0;
BOOL exists = false;
// This returns S_OK whether or not it finds a locale name. Check exists field instead.
// If it returns an error, it's a real problem, not an absence of this locale name.
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
// If we tried and it still doesn't exist, try with the fallback locale.
if (!exists)
{
localeName = FALLBACK_LOCALE;
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
}
// If it still doesn't exist, we're going to try index 0.
if (!exists)
{
index = 0;
// Get the locale name out so at least the caller knows what locale this name goes with.
UINT32 length = 0;
THROW_IF_FAILED(familyNames->GetLocaleNameLength(index, &length));
localeName.resize(length);
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalenamelength
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalename
// GetLocaleNameLength does not include space for null terminator, but GetLocaleName needs it so add one.
THROW_IF_FAILED(familyNames->GetLocaleName(index, localeName.data(), length + 1));
}
// OK, now that we've decided which family name and the locale that it's in... let's go get it.
UINT32 length = 0;
THROW_IF_FAILED(familyNames->GetStringLength(index, &length));
// Make our output buffer and resize it so it is allocated.
std::wstring retVal;
retVal.resize(length);
// FINALLY, go fetch the string name.
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstringlength
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstring
// Once again, GetStringLength is without the null, but GetString needs the null. So add one.
THROW_IF_FAILED(familyNames->GetString(index, retVal.data(), length + 1));
// and return it.
return retVal;
}
[[nodiscard]] std::wstring DxFontRenderData::_GetUserLocaleName()
{
if (_userLocaleName.empty())
{
std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> localeName;
const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow<int>(localeName.size()));
if (returnCode)
{
_userLocaleName = { localeName.data() };
}
else
{
_userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
}
}
return _userLocaleName;
}

View file

@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "../../renderer/inc/FontInfoDesired.hpp"
#include "BoxDrawingEffect.h"
#include <dwrite.h>
#include <dwrite_1.h>
#include <dwrite_2.h>
#include <dwrite_3.h>
#include <wrl.h>
namespace Microsoft::Console::Render
{
class DxFontRenderData
{
public:
struct LineMetrics
{
float gridlineWidth;
float underlineOffset;
float underlineOffset2;
float underlineWidth;
float strikethroughOffset;
float strikethroughWidth;
};
DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory) noexcept;
// DirectWrite text analyzer from the factory
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> Analyzer() noexcept;
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFallback> SystemFontFallback();
[[nodiscard]] til::size GlyphCell() noexcept;
[[nodiscard]] LineMetrics GetLineMetrics() noexcept;
// The DirectWrite format object representing the size and other text properties to be applied (by default)
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DefaultTextFormat() noexcept;
// The DirectWrite font face to use while calculating layout (by default)
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DefaultFontFace() noexcept;
// Box drawing scaling effects that are cached for the base font across layouts
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DefaultBoxDrawingEffect() noexcept;
// The italic variant of the format object representing the size and other text properties for italic text
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> ItalicTextFormat() noexcept;
// The italic variant of the font face to use while calculating layout for italic text
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> ItalicFontFace() noexcept;
[[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi) noexcept;
[[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept;
private:
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _ResolveFontFaceWithFallback(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const;
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _FindFontFace(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const;
[[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null<IDWriteFontFamily*> const fontFamily,
std::wstring& localeName) const;
// A locale that can be used on construction of assorted DX objects that want to know one.
[[nodiscard]] std::wstring _GetUserLocaleName();
::Microsoft::WRL::ComPtr<IDWriteFactory1> _dwriteFactory;
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _dwriteTextAnalyzer;
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormat;
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormatItalic;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFace;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFaceItalic;
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
::Microsoft::WRL::ComPtr<IDWriteFontFallback> _systemFontFallback;
std::wstring _userLocaleName;
til::size _glyphCell;
LineMetrics _lineMetrics;
};
}

View file

@ -51,10 +51,6 @@ D3D11_INPUT_ELEMENT_DESC _shaderInputLayout[] = {
#pragma hdrstop
static constexpr float POINTS_PER_INCH = 72.0f;
static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" };
static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us";
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
@ -82,8 +78,6 @@ DxEngine::DxEngine() :
_foregroundColor{ 0 },
_backgroundColor{ 0 },
_selectionBackground{},
_glyphCell{},
_boxDrawingEffect{},
_haveDeviceResources{ false },
_swapChainDesc{ 0 },
_swapChainFrameLatencyWaitableObject{ INVALID_HANDLE_VALUE },
@ -121,6 +115,8 @@ DxEngine::DxEngine() :
// Initialize our default selection color to DEFAULT_FOREGROUND, but make
// sure to set to to a D2D1::ColorF
SetSelectionBackground(DEFAULT_FOREGROUND);
_fontRenderData = std::make_unique<DxFontRenderData>(_dwriteFactory);
}
// Routine Description:
@ -910,9 +906,9 @@ try
{
return _dwriteFactory->CreateTextLayout(string,
gsl::narrow<UINT32>(stringLength),
_dwriteTextFormat.Get(),
_fontRenderData->DefaultTextFormat().Get(),
_displaySizePixels.width<float>(),
_glyphCell.height() != 0 ? _glyphCell.height<float>() : _displaySizePixels.height<float>(),
_fontRenderData->GlyphCell().height() != 0 ? _fontRenderData->GlyphCell().height<float>() : _displaySizePixels.height<float>(),
ppTextLayout);
}
CATCH_RETURN()
@ -936,7 +932,7 @@ try
{
_sizeTarget = Pixels;
_invalidMap.resize(_sizeTarget / _glyphCell, true);
_invalidMap.resize(_sizeTarget / _fontRenderData->GlyphCell(), true);
return S_OK;
}
CATCH_RETURN();
@ -1055,24 +1051,15 @@ try
CATCH_RETURN()
// Routine Description:
// - Invalidates one specific character coordinate
// - Invalidates the cells of the cursor
// Arguments:
// - pcoordCursor - single point in the character cell grid
// - psrRegion - the region covered by the cursor
// Return Value:
// - S_OK
[[nodiscard]] HRESULT DxEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept
try
[[nodiscard]] HRESULT DxEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, pcoordCursor);
if (!_allInvalid)
{
_InvalidateRectangle(til::rectangle{ *pcoordCursor, til::size{ 1, 1 } });
}
return S_OK;
return Invalidate(psrRegion);
}
CATCH_RETURN()
// Routine Description:
// - Invalidates a rectangle describing a pixel area on the display
@ -1089,7 +1076,7 @@ try
{
// Dirty client is in pixels. Use divide specialization against glyph factor to make conversion
// to cells.
_InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_glyphCell));
_InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_fontRenderData->GlyphCell()));
}
return S_OK;
@ -1312,7 +1299,7 @@ try
{
// Get the baseline for this font as that's where we draw from
DWRITE_LINE_SPACING spacing;
RETURN_IF_FAILED(_dwriteTextFormat->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline));
RETURN_IF_FAILED(_fontRenderData->DefaultTextFormat()->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline));
// Assemble the drawing context information
_drawingContext = std::make_unique<DrawingContext>(_d2dDeviceContext.Get(),
@ -1321,7 +1308,7 @@ try
_ShouldForceGrayscaleAA(),
_dwriteFactory.Get(),
spacing,
_glyphCell,
_fontRenderData->GlyphCell(),
_d2dDeviceContext->GetSize(),
std::nullopt,
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT);
@ -1363,14 +1350,14 @@ try
// Scale all dirty rectangles into pixels
std::transform(_presentDirty.begin(), _presentDirty.end(), _presentDirty.begin(), [&](til::rectangle rc) {
return rc.scale_up(_glyphCell);
return rc.scale_up(_fontRenderData->GlyphCell());
});
// Invalid scroll is in characters, convert it to pixels.
const auto scrollPixels = (_invalidScroll * _glyphCell);
const auto scrollPixels = (_invalidScroll * _fontRenderData->GlyphCell());
// The scroll rect is the entire field of cells, but in pixels.
til::rectangle scrollArea{ _invalidMap.size() * _glyphCell };
til::rectangle scrollArea{ _invalidMap.size() * _fontRenderData->GlyphCell() };
// Reduce the size of the rectangle by the scroll.
scrollArea -= til::size{} - scrollPixels;
@ -1618,7 +1605,7 @@ try
// Runs are counts of cells.
// Use a transform by the size of one cell to convert cells-to-pixels
// as we clear.
_d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Scale(_glyphCell));
_d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Scale(_fontRenderData->GlyphCell()));
for (const auto& rect : _invalidMap.runs())
{
// Use aliased.
@ -1652,7 +1639,7 @@ CATCH_RETURN()
try
{
// Calculate positioning of our origin.
const D2D1_POINT_2F origin = til::point{ coord } * _glyphCell;
const D2D1_POINT_2F origin = til::point{ coord } * _fontRenderData->GlyphCell();
// Create the text layout
RETURN_IF_FAILED(_customLayout->Reset());
@ -1686,7 +1673,7 @@ try
_d2dBrushForeground->SetColor(_ColorFFromColorRef(color));
const D2D1_SIZE_F font = _glyphCell;
const D2D1_SIZE_F font = _fontRenderData->GlyphCell();
const D2D_POINT_2F target = { coordTarget.X * font.width, coordTarget.Y * font.height };
const auto fullRunWidth = font.width * gsl::narrow_cast<unsigned>(cchLine);
@ -1701,10 +1688,10 @@ try
// NOTE: Line coordinates are centered within the line, so they need to be
// offset by half the stroke width. For the start coordinate we add half
// the stroke width, and for the end coordinate we subtract half the width.
const DxFontRenderData::LineMetrics lineMetrics = _fontRenderData->GetLineMetrics();
if (WI_IsAnyFlagSet(lines, (GridLines::Left | GridLines::Right)))
{
const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f;
const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f;
const auto startY = target.y + halfGridlineWidth;
const auto endY = target.y + font.height - halfGridlineWidth;
@ -1713,7 +1700,7 @@ try
auto x = target.x + halfGridlineWidth;
for (size_t i = 0; i < cchLine; i++, x += font.width)
{
DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth);
DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth);
}
}
@ -1722,27 +1709,27 @@ try
auto x = target.x + font.width - halfGridlineWidth;
for (size_t i = 0; i < cchLine; i++, x += font.width)
{
DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth);
DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth);
}
}
}
if (WI_IsAnyFlagSet(lines, GridLines::Top | GridLines::Bottom))
{
const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f;
const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f;
const auto startX = target.x + halfGridlineWidth;
const auto endX = target.x + fullRunWidth - halfGridlineWidth;
if (WI_IsFlagSet(lines, GridLines::Top))
{
const auto y = target.y + halfGridlineWidth;
DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth);
DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth);
}
if (WI_IsFlagSet(lines, GridLines::Bottom))
{
const auto y = target.y + font.height - halfGridlineWidth;
DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth);
DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth);
}
}
@ -1751,37 +1738,37 @@ try
if (WI_IsAnyFlagSet(lines, GridLines::Underline | GridLines::DoubleUnderline | GridLines::HyperlinkUnderline))
{
const auto halfUnderlineWidth = _lineMetrics.underlineWidth / 2.0f;
const auto halfUnderlineWidth = lineMetrics.underlineWidth / 2.0f;
const auto startX = target.x + halfUnderlineWidth;
const auto endX = target.x + fullRunWidth - halfUnderlineWidth;
const auto y = target.y + _lineMetrics.underlineOffset;
const auto y = target.y + lineMetrics.underlineOffset;
if (WI_IsFlagSet(lines, GridLines::Underline))
{
DrawLine(startX, y, endX, y, _lineMetrics.underlineWidth);
DrawLine(startX, y, endX, y, lineMetrics.underlineWidth);
}
if (WI_IsFlagSet(lines, GridLines::HyperlinkUnderline))
{
DrawHyperlinkLine(startX, y, endX, y, _lineMetrics.underlineWidth);
DrawHyperlinkLine(startX, y, endX, y, lineMetrics.underlineWidth);
}
if (WI_IsFlagSet(lines, GridLines::DoubleUnderline))
{
DrawLine(startX, y, endX, y, _lineMetrics.underlineWidth);
const auto y2 = target.y + _lineMetrics.underlineOffset2;
DrawLine(startX, y2, endX, y2, _lineMetrics.underlineWidth);
DrawLine(startX, y, endX, y, lineMetrics.underlineWidth);
const auto y2 = target.y + lineMetrics.underlineOffset2;
DrawLine(startX, y2, endX, y2, lineMetrics.underlineWidth);
}
}
if (WI_IsFlagSet(lines, GridLines::Strikethrough))
{
const auto halfStrikethroughWidth = _lineMetrics.strikethroughWidth / 2.0f;
const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f;
const auto startX = target.x + halfStrikethroughWidth;
const auto endX = target.x + fullRunWidth - halfStrikethroughWidth;
const auto y = target.y + _lineMetrics.strikethroughOffset;
const auto y = target.y + lineMetrics.strikethroughOffset;
DrawLine(startX, y, endX, y, _lineMetrics.strikethroughWidth);
DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth);
}
return S_OK;
@ -1805,7 +1792,7 @@ try
_d2dBrushForeground->SetColor(_selectionBackground);
const auto resetColorOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); });
const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_glyphCell);
const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_fontRenderData->GlyphCell());
_d2dDeviceContext->FillRectangle(draw, _d2dBrushForeground.Get());
@ -1965,30 +1952,10 @@ CATCH_RETURN()
[[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept
try
{
RETURN_IF_FAILED(_GetProposedFont(pfiFontInfoDesired,
fiFontInfo,
_dpi,
_dwriteTextFormat,
_dwriteTextFormatItalic,
_dwriteTextAnalyzer,
_dwriteFontFace,
_dwriteFontFaceItalic,
_lineMetrics));
_glyphCell = fiFontInfo.GetSize();
// Calculate and cache the box effect for the base font. Scale is 1.0f because the base font is exactly the scale we want already.
RETURN_IF_FAILED(CustomTextLayout::s_CalculateBoxEffect(_dwriteTextFormat.Get(), _glyphCell.width(), _dwriteFontFace.Get(), 1.0f, &_boxDrawingEffect));
RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi));
// Prepare the text layout.
_customLayout = WRL::Make<CustomTextLayout>(_dwriteFactory.Get(),
_dwriteTextAnalyzer.Get(),
_dwriteTextFormat.Get(),
_dwriteTextFormatItalic.Get(),
_dwriteFontFace.Get(),
_dwriteFontFaceItalic.Get(),
_glyphCell.width(),
_boxDrawingEffect.Get());
_customLayout = WRL::Make<CustomTextLayout>(_fontRenderData.get());
return S_OK;
}
@ -1996,16 +1963,16 @@ CATCH_RETURN();
[[nodiscard]] Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) noexcept
{
const short widthInChars = base::saturated_cast<short>(viewInPixels.Width() / _glyphCell.width());
const short heightInChars = base::saturated_cast<short>(viewInPixels.Height() / _glyphCell.height());
const short widthInChars = base::saturated_cast<short>(viewInPixels.Width() / _fontRenderData->GlyphCell().width());
const short heightInChars = base::saturated_cast<short>(viewInPixels.Height() / _fontRenderData->GlyphCell().height());
return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars });
}
[[nodiscard]] Viewport DxEngine::GetViewportInPixels(const Viewport& viewInCharacters) noexcept
{
const short widthInPixels = base::saturated_cast<short>(viewInCharacters.Width() * _glyphCell.width());
const short heightInPixels = base::saturated_cast<short>(viewInCharacters.Height() * _glyphCell.height());
const short widthInPixels = base::saturated_cast<short>(viewInCharacters.Width() * _fontRenderData->GlyphCell().width());
const short heightInPixels = base::saturated_cast<short>(viewInCharacters.Height() * _fontRenderData->GlyphCell().height());
return Viewport::FromDimensions(viewInCharacters.Origin(), { widthInPixels, heightInPixels });
}
@ -2067,22 +2034,8 @@ float DxEngine::GetScaling() const noexcept
FontInfo& pfiFontInfo,
int const iDpi) noexcept
{
Microsoft::WRL::ComPtr<IDWriteTextFormat> format;
Microsoft::WRL::ComPtr<IDWriteTextFormat> formatItalic;
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> analyzer;
Microsoft::WRL::ComPtr<IDWriteFontFace1> face;
Microsoft::WRL::ComPtr<IDWriteFontFace1> faceItalic;
LineMetrics lineMetrics;
return _GetProposedFont(pfiFontInfoDesired,
pfiFontInfo,
iDpi,
format,
formatItalic,
analyzer,
face,
faceItalic,
lineMetrics);
DxFontRenderData fontRenderData(_dwriteFactory);
return fontRenderData.UpdateFont(pfiFontInfoDesired, pfiFontInfo, iDpi);
}
// Routine Description:
@ -2108,7 +2061,7 @@ CATCH_RETURN();
[[nodiscard]] HRESULT DxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept
try
{
*pFontSize = _glyphCell;
*pFontSize = _fontRenderData->GlyphCell();
return S_OK;
}
CATCH_RETURN();
@ -2154,447 +2107,6 @@ CATCH_RETURN();
return S_FALSE;
}
// Routine Description:
// - Attempts to locate the font given, but then begins falling back if we cannot find it.
// - We'll try to fall back to Consolas with the given weight/stretch/style first,
// then try Consolas again with normal weight/stretch/style,
// and if nothing works, then we'll throw an error.
// Arguments:
// - familyName - The font name we should be looking for
// - weight - The weight (bold, light, etc.)
// - stretch - The stretch of the font is the spacing between each letter
// - style - Normal, italic, etc.
// Return Value:
// - Smart pointer holding interface reference for queryable font data.
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxEngine::_ResolveFontFaceWithFallback(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const
{
auto face = _FindFontFace(familyName, weight, stretch, style, localeName);
if (!face)
{
for (const auto fallbackFace : FALLBACK_FONT_FACES)
{
familyName = fallbackFace;
face = _FindFontFace(familyName, weight, stretch, style, localeName);
if (face)
{
break;
}
familyName = fallbackFace;
weight = DWRITE_FONT_WEIGHT_NORMAL;
stretch = DWRITE_FONT_STRETCH_NORMAL;
style = DWRITE_FONT_STYLE_NORMAL;
face = _FindFontFace(familyName, weight, stretch, style, localeName);
if (face)
{
break;
}
}
}
THROW_HR_IF_NULL(E_FAIL, face);
return face;
}
// Routine Description:
// - Locates a suitable font face from the given information
// Arguments:
// - familyName - The font name we should be looking for
// - weight - The weight (bold, light, etc.)
// - stretch - The stretch of the font is the spacing between each letter
// - style - Normal, italic, etc.
// Return Value:
// - Smart pointer holding interface reference for queryable font data.
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxEngine::_FindFontFace(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const
{
Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
Microsoft::WRL::ComPtr<IDWriteFontCollection> fontCollection;
THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false));
UINT32 familyIndex;
BOOL familyExists;
THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists));
if (familyExists)
{
Microsoft::WRL::ComPtr<IDWriteFontFamily> fontFamily;
THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily));
Microsoft::WRL::ComPtr<IDWriteFont> font;
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font));
Microsoft::WRL::ComPtr<IDWriteFontFace> fontFace0;
THROW_IF_FAILED(font->CreateFontFace(&fontFace0));
THROW_IF_FAILED(fontFace0.As(&fontFace));
// Retrieve metrics in case the font we created was different than what was requested.
weight = font->GetWeight();
stretch = font->GetStretch();
style = font->GetStyle();
// Dig the family name out at the end to return it.
familyName = _GetFontFamilyName(fontFamily.Get(), localeName);
}
return fontFace;
}
// Routine Description:
// - Helper to retrieve the user's locale preference or fallback to the default.
// Arguments:
// - <none>
// Return Value:
// - A locale that can be used on construction of assorted DX objects that want to know one.
[[nodiscard]] std::wstring DxEngine::_GetLocaleName() const
{
std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> localeName;
const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow<int>(localeName.size()));
if (returnCode)
{
return { localeName.data() };
}
else
{
return { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
}
}
// Routine Description:
// - Retrieves the font family name out of the given object in the given locale.
// - If we can't find a valid name for the given locale, we'll fallback and report it back.
// Arguments:
// - fontFamily - DirectWrite font family object
// - localeName - The locale in which the name should be retrieved.
// - If fallback occurred, this is updated to what we retrieved instead.
// Return Value:
// - Localized string name of the font family
[[nodiscard]] std::wstring DxEngine::_GetFontFamilyName(gsl::not_null<IDWriteFontFamily*> const fontFamily,
std::wstring& localeName) const
{
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
Microsoft::WRL::ComPtr<IDWriteLocalizedStrings> familyNames;
THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames));
// First we have to find the right family name for the locale. We're going to bias toward what the caller
// requested, but fallback if we need to and reply with the locale we ended up choosing.
UINT32 index = 0;
BOOL exists = false;
// This returns S_OK whether or not it finds a locale name. Check exists field instead.
// If it returns an error, it's a real problem, not an absence of this locale name.
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
// If we tried and it still doesn't exist, try with the fallback locale.
if (!exists)
{
localeName = FALLBACK_LOCALE;
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
}
// If it still doesn't exist, we're going to try index 0.
if (!exists)
{
index = 0;
// Get the locale name out so at least the caller knows what locale this name goes with.
UINT32 length = 0;
THROW_IF_FAILED(familyNames->GetLocaleNameLength(index, &length));
localeName.resize(length);
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalenamelength
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalename
// GetLocaleNameLength does not include space for null terminator, but GetLocaleName needs it so add one.
THROW_IF_FAILED(familyNames->GetLocaleName(index, localeName.data(), length + 1));
}
// OK, now that we've decided which family name and the locale that it's in... let's go get it.
UINT32 length = 0;
THROW_IF_FAILED(familyNames->GetStringLength(index, &length));
// Make our output buffer and resize it so it is allocated.
std::wstring retVal;
retVal.resize(length);
// FINALLY, go fetch the string name.
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstringlength
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstring
// Once again, GetStringLength is without the null, but GetString needs the null. So add one.
THROW_IF_FAILED(familyNames->GetString(index, retVal.data(), length + 1));
// and return it.
return retVal;
}
// Routine Description:
// - Updates the font used for drawing
// Arguments:
// - desired - Information specifying the font that is requested
// - actual - Filled with the nearest font actually chosen for drawing
// - dpi - The DPI of the screen
// Return Value:
// - S_OK or relevant DirectX error
[[nodiscard]] HRESULT DxEngine::_GetProposedFont(const FontInfoDesired& desired,
FontInfo& actual,
const int dpi,
Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormat,
Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormatItalic,
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1>& textAnalyzer,
Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFace,
Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFaceItalic,
LineMetrics& lineMetrics) const noexcept
{
try
{
std::wstring fontName(desired.GetFaceName());
DWRITE_FONT_WEIGHT weight = static_cast<DWRITE_FONT_WEIGHT>(desired.GetWeight());
DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL;
DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL;
std::wstring localeName = _GetLocaleName();
// _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font,
// but we should use the system's locale to render the text.
std::wstring fontLocaleName = localeName;
const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName);
DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
const UINT32 spaceCodePoint = L'M';
UINT16 spaceGlyphIndex;
THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex));
INT32 advanceInDesignUnits;
THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits));
DWRITE_GLYPH_METRICS spaceMetrics = { 0 };
THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics));
// The math here is actually:
// Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor.
// - DPI = dots per inch
// - PPI = points per inch or "points" as usually seen when choosing a font size
// - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI.
// - The Points to Pixels factor is based on the typography definition of 72 points per inch.
// As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch
// to get a factor of 1 and 1/3.
// This turns into something like:
// - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%)
// - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%)
// - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%)
float heightDesired = static_cast<float>(desired.GetEngineSize().Y) * static_cast<float>(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH;
// The advance is the number of pixels left-to-right (X dimension) for the given font.
// We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement.
// Now we play trickery with the font size. Scale by the DPI to get the height we expect.
heightDesired *= (static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI));
const float widthAdvance = static_cast<float>(advanceInDesignUnits) / fontMetrics.designUnitsPerEm;
// Use the real pixel height desired by the "em" factor for the width to get the number of pixels
// we will need per character in width. This will almost certainly result in fractional X-dimension pixels.
const float widthApprox = heightDesired * widthAdvance;
// Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel.
const float widthExact = round(widthApprox);
// Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional
// height in pixels of each character. It's easier for us to pad out height and align vertically
// than it is horizontally.
const auto fontSize = widthExact / widthAdvance;
// Now figure out the basic properties of the character height which include ascent and descent
// for this specific font size.
const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm;
const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm;
// Get the gap.
const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm;
const float halfGap = gap / 2;
// We're going to build a line spacing object here to track all of this data in our format.
DWRITE_LINE_SPACING lineSpacing = {};
lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM;
// We need to make sure the baseline falls on a round pixel (not a fractional pixel).
// If the baseline is fractional, the text appears blurry, especially at small scales.
// Since we also need to make sure the bounding box as a whole is round pixels
// (because the entire console system maths in full cell units),
// we're just going to ceiling up the ascent and descent to make a full pixel amount
// and set the baseline to the full round pixel ascent value.
//
// For reference, for the letters "ag":
// ...
// gggggg bottom of previous line
//
// ----------------- <===========================================|
// | topSideBearing | 1/2 lineGap |
// aaaaaa ggggggg <-------------------------|-------------| |
// a g g | | |
// aaaaa ggggg |<-ascent | |
// a a g | | |---- lineHeight
// aaaaa a gggggg <----baseline, verticalOriginY----------|---|
// g g |<-descent | |
// gggggg <-------------------------|-------------| |
// | bottomSideBearing | 1/2 lineGap |
// ----------------- <===========================================|
//
// aaaaaa ggggggg top of next line
// ...
//
// Also note...
// We're going to add half the line gap to the ascent and half the line gap to the descent
// to ensure that the spacing is balanced vertically.
// Generally speaking, the line gap is added to the ascent by DirectWrite itself for
// horizontally drawn text which can place the baseline and glyphs "lower" in the drawing
// box than would be desired for proper alignment of things like line and box characters
// which will try to sit centered in the area and touch perfectly with their neighbors.
const auto fullPixelAscent = ceil(ascent + halfGap);
const auto fullPixelDescent = ceil(descent + halfGap);
lineSpacing.height = fullPixelAscent + fullPixelDescent;
lineSpacing.baseline = fullPixelAscent;
// According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage)
// Setting "ENABLED" means we've included the line gapping in the spacing numbers given.
lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED;
// Create the font with the fractional pixel height size.
// It should have an integer pixel width by our math above.
// Then below, apply the line spacing to the format to position the floating point pixel height characters
// into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out.
Microsoft::WRL::ComPtr<IDWriteTextFormat> format;
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontName.data(),
nullptr,
weight,
style,
stretch,
fontSize,
localeName.data(),
&format));
THROW_IF_FAILED(format.As(&textFormat));
// We also need to create an italic variant of the font face and text
// format, based on the same parameters, but using an italic style.
std::wstring fontNameItalic = fontName;
DWRITE_FONT_WEIGHT weightItalic = weight;
DWRITE_FONT_STYLE styleItalic = DWRITE_FONT_STYLE_ITALIC;
DWRITE_FONT_STRETCH stretchItalic = stretch;
const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName);
Microsoft::WRL::ComPtr<IDWriteTextFormat> formatItalic;
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(),
nullptr,
weightItalic,
styleItalic,
stretchItalic,
fontSize,
localeName.data(),
&formatItalic));
THROW_IF_FAILED(formatItalic.As(&textFormatItalic));
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer> analyzer;
THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
THROW_IF_FAILED(analyzer.As(&textAnalyzer));
fontFace = face;
fontFaceItalic = faceItalic;
THROW_IF_FAILED(textFormat->SetLineSpacing(lineSpacing.method, lineSpacing.height, lineSpacing.baseline));
THROW_IF_FAILED(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR));
THROW_IF_FAILED(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
// The scaled size needs to represent the pixel box that each character will fit within for the purposes
// of hit testing math and other such multiplication/division.
COORD coordSize = { 0 };
coordSize.X = gsl::narrow<SHORT>(widthExact);
coordSize.Y = gsl::narrow_cast<SHORT>(lineSpacing.height);
// Unscaled is for the purposes of re-communicating this font back to the renderer again later.
// As such, we need to give the same original size parameter back here without padding
// or rounding or scaling manipulation.
const COORD unscaled = desired.GetEngineSize();
const COORD scaled = coordSize;
actual.SetFromEngine(fontName,
desired.GetFamily(),
textFormat->GetFontWeight(),
false,
scaled,
unscaled);
// There is no font metric for the grid line width, so we use a small
// multiple of the font size, which typically rounds to a pixel.
lineMetrics.gridlineWidth = std::round(fontSize * 0.025f);
// All other line metrics are in design units, so to get a pixel value,
// we scale by the font size divided by the design-units-per-em.
const auto scale = fontSize / fontMetrics.designUnitsPerEm;
lineMetrics.underlineOffset = std::round(fontMetrics.underlinePosition * scale);
lineMetrics.underlineWidth = std::round(fontMetrics.underlineThickness * scale);
lineMetrics.strikethroughOffset = std::round(fontMetrics.strikethroughPosition * scale);
lineMetrics.strikethroughWidth = std::round(fontMetrics.strikethroughThickness * scale);
// We always want the lines to be visible, so if a stroke width ends up
// at zero after rounding, we need to make it at least 1 pixel.
lineMetrics.gridlineWidth = std::max(lineMetrics.gridlineWidth, 1.0f);
lineMetrics.underlineWidth = std::max(lineMetrics.underlineWidth, 1.0f);
lineMetrics.strikethroughWidth = std::max(lineMetrics.strikethroughWidth, 1.0f);
// Offsets are relative to the base line of the font, so we subtract
// from the ascent to get an offset relative to the top of the cell.
lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset;
lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset;
// For double underlines we need a second offset, just below the first,
// but with a bit of a gap (about double the grid line width).
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset +
lineMetrics.underlineWidth +
std::round(fontSize * 0.05f);
// However, we don't want the underline to extend past the bottom of the
// cell, so we clamp the offset to fit just inside.
const auto maxUnderlineOffset = lineSpacing.height - _lineMetrics.underlineWidth;
lineMetrics.underlineOffset2 = std::min(lineMetrics.underlineOffset2, maxUnderlineOffset);
// But if the resulting gap isn't big enough even to register as a thicker
// line, it's better to place the second line slightly above the first.
if (lineMetrics.underlineOffset2 < lineMetrics.underlineOffset + lineMetrics.gridlineWidth)
{
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth;
}
// We also add half the stroke width to the offsets, since the line
// coordinates designate the center of the line.
lineMetrics.underlineOffset += lineMetrics.underlineWidth / 2.0f;
lineMetrics.underlineOffset2 += lineMetrics.underlineWidth / 2.0f;
lineMetrics.strikethroughOffset += lineMetrics.strikethroughWidth / 2.0f;
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - Helps convert a GDI COLORREF into a Direct2D ColorF
// Arguments:

View file

@ -26,6 +26,7 @@
#include "CustomTextLayout.h"
#include "CustomTextRenderer.h"
#include "DxFontRenderData.h"
#include "../../types/inc/Viewport.hpp"
@ -73,7 +74,7 @@ namespace Microsoft::Console::Render
// IRenderEngine Members
[[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;
@ -155,20 +156,7 @@ namespace Microsoft::Console::Render
bool _isEnabled;
bool _isPainting;
struct LineMetrics
{
float gridlineWidth;
float underlineOffset;
float underlineOffset2;
float underlineWidth;
float strikethroughOffset;
float strikethroughWidth;
};
LineMetrics _lineMetrics;
til::size _displaySizePixels;
til::size _glyphCell;
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
D2D1_COLOR_F _defaultForegroundColor;
D2D1_COLOR_F _defaultBackgroundColor;
@ -198,17 +186,14 @@ namespace Microsoft::Console::Render
::Microsoft::WRL::ComPtr<ID2D1Factory1> _d2dFactory;
::Microsoft::WRL::ComPtr<IDWriteFactory1> _dwriteFactory;
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormat;
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormatItalic;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFace;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFaceItalic;
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _dwriteTextAnalyzer;
::Microsoft::WRL::ComPtr<CustomTextLayout> _customLayout;
::Microsoft::WRL::ComPtr<CustomTextRenderer> _customRenderer;
::Microsoft::WRL::ComPtr<ID2D1StrokeStyle> _strokeStyle;
::Microsoft::WRL::ComPtr<ID2D1StrokeStyle> _dashStrokeStyle;
::Microsoft::WRL::ComPtr<ID2D1StrokeStyle> _hyperlinkStrokeStyle;
std::unique_ptr<DxFontRenderData> _fontRenderData;
D2D1_STROKE_STYLE_PROPERTIES _strokeStyleProperties;
D2D1_STROKE_STYLE_PROPERTIES _dashStrokeStyleProperties;
@ -303,33 +288,6 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT _EnableDisplayAccess(const bool outputEnabled) noexcept;
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _ResolveFontFaceWithFallback(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const;
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _FindFontFace(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const;
[[nodiscard]] std::wstring _GetLocaleName() const;
[[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null<IDWriteFontFamily*> const fontFamily,
std::wstring& localeName) const;
[[nodiscard]] HRESULT _GetProposedFont(const FontInfoDesired& desired,
FontInfo& actual,
const int dpi,
::Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormat,
::Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormatItalic,
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1>& textAnalyzer,
::Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFace,
::Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFaceItalic,
LineMetrics& lineMetrics) const noexcept;
[[nodiscard]] til::size _GetClientSize() const;
void _InvalidateRectangle(const til::rectangle& rc);

View file

@ -21,6 +21,7 @@
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\DxFontRenderData.cpp" />
<ClCompile Include="..\DxRenderer.cpp" />
</ItemGroup>
<ItemGroup>
@ -29,6 +30,7 @@
<ClInclude Include="..\CustomTextRenderer.h" />
<ClInclude Include="..\precomp.h" />
<ClInclude Include="..\DxRenderer.hpp" />
<ClInclude Include="..\DxFontRenderData.h" />
<ClInclude Include="..\ScreenPixelShader.h" />
<ClInclude Include="..\ScreenVertexShader.h" />
</ItemGroup>

View file

@ -7,6 +7,7 @@
<ClCompile Include="..\CustomTextLayout.cpp" />
<ClCompile Include="..\CustomTextRenderer.cpp" />
<ClCompile Include="..\precomp.cpp" />
<ClCompile Include="..\DxFontRenderData.cpp" />
<ClCompile Include="..\DxRenderer.cpp" />
<ClCompile Include="..\BoxDrawingEffect.cpp" />
</ItemGroup>
@ -14,6 +15,7 @@
<ClInclude Include="..\CustomTextLayout.h" />
<ClInclude Include="..\CustomTextRenderer.h" />
<ClInclude Include="..\precomp.h" />
<ClInclude Include="..\DxFontRenderData.h"/>
<ClInclude Include="..\DxRenderer.hpp" />
<ClInclude Include="..\ScreenPixelShader.h" />
<ClInclude Include="..\ScreenVertexShader.h" />

View file

@ -33,6 +33,7 @@ INCLUDES = \
SOURCES = \
$(SOURCES) \
..\DxRenderer.cpp \
..\DxFontRenderData.cpp \
..\CustomTextRenderer.cpp \
..\CustomTextLayout.cpp \

View file

@ -30,7 +30,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;
[[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override;
[[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;
@ -41,6 +41,11 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT ScrollFrame() noexcept override;
[[nodiscard]] HRESULT ResetLineTransform() noexcept override;
[[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
const size_t targetRow,
const size_t viewportLeft) noexcept override;
[[nodiscard]] HRESULT PaintBackground() noexcept override;
[[nodiscard]] HRESULT PaintBufferLine(gsl::span<const Cluster> const clusters,
const COORD coord,
@ -97,6 +102,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT _FlushBufferLines() noexcept;
std::vector<RECT> cursorInvertRects;
XFORM cursorInvertTransform;
struct LineMetrics
{
@ -126,6 +132,9 @@ namespace Microsoft::Console::Render
COLORREF _lastBg;
bool _lastFontItalic;
XFORM _currentLineTransform;
LineRendition _currentLineRendition;
// Memory pooling to save alloc/free work to the OS for things
// frequently created and dropped.
// It's important the pool is first so it can be given to the others on construction.
@ -183,4 +192,11 @@ namespace Microsoft::Console::Render
HDC _debugContext;
#endif
};
constexpr XFORM IDENTITY_XFORM = { 1, 0, 0, 1 };
inline bool operator==(const XFORM& lhs, const XFORM& rhs) noexcept
{
return ::memcmp(&lhs, &rhs, sizeof(XFORM)) == 0;
};
}

View file

@ -81,13 +81,12 @@ HRESULT GdiEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept
// Routine Description:
// - Notifies us that the console has changed the position of the cursor.
// Arguments:
// - pcoordCursor - the new position of the cursor
// - psrRegion - the region covered by the cursor
// Return Value:
// - S_OK, else an appropriate HRESULT for failing to allocate or write.
HRESULT GdiEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept
HRESULT GdiEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept
{
SMALL_RECT sr = Viewport::FromCoord(*pcoordCursor).ToExclusive();
return this->Invalidate(&sr);
return this->Invalidate(psrRegion);
}
// Routine Description:

View file

@ -41,6 +41,9 @@ using namespace Microsoft::Console::Render;
_psInvalidData.hdc = GetDC(_hwndTargetWindow);
RETURN_HR_IF_NULL(E_FAIL, _psInvalidData.hdc);
// We need the advanced graphics mode in order to set a transform.
SetGraphicsMode(_psInvalidData.hdc, GM_ADVANCED);
// Signal that we're starting to paint.
_fPaintStarted = true;
@ -71,11 +74,28 @@ using namespace Microsoft::Console::Render;
// left behind cursor copies in the scrolled region.
if (cursorInvertRects.size() > 0)
{
// We first need to apply the transform that was active at the time the cursor
// was rendered otherwise we won't be clearing the right area of the display.
// We don't need to do this if it was an identity transform though.
const bool identityTransform = cursorInvertTransform == IDENTITY_XFORM;
if (!identityTransform)
{
LOG_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &cursorInvertTransform));
LOG_HR_IF(E_FAIL, !SetWorldTransform(_psInvalidData.hdc, &cursorInvertTransform));
}
for (RECT r : cursorInvertRects)
{
// Clean both the in-memory and actual window context.
RETURN_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r)));
RETURN_HR_IF(E_FAIL, !(InvertRect(_psInvalidData.hdc, &r)));
LOG_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r)));
LOG_HR_IF(E_FAIL, !(InvertRect(_psInvalidData.hdc, &r)));
}
// If we've applied a transform, then we need to reset it.
if (!identityTransform)
{
LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_hdcMemoryContext, nullptr, MWT_IDENTITY));
LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_psInvalidData.hdc, nullptr, MWT_IDENTITY));
}
cursorInvertRects.clear();
@ -369,15 +389,21 @@ using namespace Microsoft::Console::Render;
}
}
// If the line rendition is double height, we need to adjust the top or bottom
// of the clipping rect to clip half the height of the rendered characters.
const auto halfHeight = coordFontSize.Y >> 1;
const auto topOffset = _currentLineRendition == LineRendition::DoubleHeightBottom ? halfHeight : 0;
const auto bottomOffset = _currentLineRendition == LineRendition::DoubleHeightTop ? halfHeight : 0;
pPolyTextLine->lpstr = polyString.data();
pPolyTextLine->n = gsl::narrow<UINT>(clusters.size());
pPolyTextLine->x = ptDraw.x;
pPolyTextLine->y = ptDraw.y;
pPolyTextLine->uiFlags = ETO_OPAQUE | ETO_CLIPPED;
pPolyTextLine->rcl.left = pPolyTextLine->x;
pPolyTextLine->rcl.top = pPolyTextLine->y;
pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + ((SHORT)cchCharWidths * coordFontSize.X);
pPolyTextLine->rcl.bottom = pPolyTextLine->rcl.top + coordFontSize.Y;
pPolyTextLine->rcl.top = pPolyTextLine->y + topOffset;
pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + (SHORT)cchCharWidths;
pPolyTextLine->rcl.bottom = pPolyTextLine->y + coordFontSize.Y - bottomOffset;
pPolyTextLine->pdx = polyWidth.data();
if (trimLeft)
@ -628,6 +654,13 @@ using namespace Microsoft::Console::Render;
default:
return E_NOTIMPL;
}
// Prepare the appropriate line transform for the current row.
LOG_IF_FAILED(PrepareLineTransform(options.lineRendition, 0, options.viewportLeft));
auto resetLineTransform = wil::scope_exit([&]() {
LOG_IF_FAILED(ResetLineTransform());
});
// Either invert all the RECTs, or paint them.
if (options.fUseColor)
{
@ -642,6 +675,10 @@ using namespace Microsoft::Console::Render;
}
else
{
// Save the current line transform in case we need to reapply these
// inverted rects to hide the cursor in the ScrollFrame method.
cursorInvertTransform = _currentLineTransform;
for (RECT r : cursorInvertRects)
{
RETURN_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r)));

View file

@ -30,6 +30,8 @@ GdiEngine::GdiEngine() :
_lastFg(INVALID_COLOR),
_lastBg(INVALID_COLOR),
_lastFontItalic(false),
_currentLineTransform(IDENTITY_XFORM),
_currentLineRendition(LineRendition::SingleWidth),
_fPaintStarted(false),
_invalidCharacters{},
_hfont(nullptr),
@ -46,6 +48,9 @@ GdiEngine::GdiEngine() :
_hdcMemoryContext = CreateCompatibleDC(nullptr);
THROW_HR_IF_NULL(E_FAIL, _hdcMemoryContext);
// We need the advanced graphics mode in order to set a transform.
SetGraphicsMode(_hdcMemoryContext, GM_ADVANCED);
// On session zero, text GDI APIs might not be ready.
// Calling GetTextFace causes a wait that will be
// satisfied while GDI text APIs come online.
@ -123,6 +128,9 @@ GdiEngine::~GdiEngine()
HDC const hdcNewMemoryContext = CreateCompatibleDC(hdcRealWindow);
RETURN_HR_IF_NULL(E_FAIL, hdcNewMemoryContext);
// We need the advanced graphics mode in order to set a transform.
SetGraphicsMode(hdcNewMemoryContext, GM_ADVANCED);
// If we had an existing memory context stored, release it before proceeding.
if (nullptr != _hdcMemoryContext)
{
@ -185,6 +193,77 @@ GdiEngine::~GdiEngine()
return S_OK;
}
// Routine Description
// - Resets the world transform to the identity matrix.
// Arguments:
// - <none>
// Return Value:
// - S_OK if successful. S_FALSE if already reset. E_FAIL if there was an error.
[[nodiscard]] HRESULT GdiEngine::ResetLineTransform() noexcept
{
// Return early if the current transform is already the identity matrix.
RETURN_HR_IF(S_FALSE, _currentLineTransform == IDENTITY_XFORM);
// Flush any buffer lines which would be expecting to use the current transform.
LOG_IF_FAILED(_FlushBufferLines());
// Reset the active transform to the identity matrix.
RETURN_HR_IF(E_FAIL, !ModifyWorldTransform(_hdcMemoryContext, nullptr, MWT_IDENTITY));
// Reset the current state.
_currentLineTransform = IDENTITY_XFORM;
_currentLineRendition = LineRendition::SingleWidth;
return S_OK;
}
// Routine Description
// - Applies an appropriate transform for the given line rendition and viewport offset.
// Arguments:
// - lineRendition - The line rendition specifying the scaling of the line.
// - targetRow - The row on which the line is expected to be rendered.
// - viewportLeft - The left offset of the current viewport.
// Return Value:
// - S_OK if successful. S_FALSE if already set. E_FAIL if there was an error.
[[nodiscard]] HRESULT GdiEngine::PrepareLineTransform(const LineRendition lineRendition,
const size_t targetRow,
const size_t viewportLeft) noexcept
{
XFORM lineTransform = {};
// The X delta is to account for the horizontal viewport offset.
lineTransform.eDx = viewportLeft ? -1.0f * viewportLeft * _GetFontSize().X : 0.0f;
switch (lineRendition)
{
case LineRendition::SingleWidth:
lineTransform.eM11 = 1; // single width
lineTransform.eM22 = 1; // single height
break;
case LineRendition::DoubleWidth:
lineTransform.eM11 = 2; // double width
lineTransform.eM22 = 1; // single height
break;
case LineRendition::DoubleHeightTop:
lineTransform.eM11 = 2; // double width
lineTransform.eM22 = 2; // double height
// The Y delta is to negate the offset caused by the scaled height.
lineTransform.eDy = -1.0f * targetRow * _GetFontSize().Y;
break;
case LineRendition::DoubleHeightBottom:
lineTransform.eM11 = 2; // double width
lineTransform.eM22 = 2; // double height
// The Y delta is to negate the offset caused by the scaled height.
// An extra row is added because we need the bottom half of the line.
lineTransform.eDy = -1.0f * (targetRow + 1) * _GetFontSize().Y;
break;
}
// Return early if the new matrix is the same as the current transform.
RETURN_HR_IF(S_FALSE, _currentLineRendition == lineRendition && _currentLineTransform == lineTransform);
// Flush any buffer lines which would be expecting to use the current transform.
LOG_IF_FAILED(_FlushBufferLines());
// Set the active transform with the new matrix.
RETURN_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &lineTransform));
// Save the current state.
_currentLineTransform = lineTransform;
_currentLineRendition = lineRendition;
return S_OK;
}
// Routine Description:
// - This method will set the GDI brushes in the drawing context (and update the hung-window background color)
// Arguments:

View file

@ -14,6 +14,7 @@ Author(s):
#pragma once
#include "../../buffer/out/LineRendition.hpp"
#include "../../inc/conattrs.hpp"
namespace Microsoft::Console::Render
@ -21,9 +22,15 @@ namespace Microsoft::Console::Render
struct CursorOptions
{
// Character cell in the grid to draw at
// This is relative to the viewport, not the buffer.
// This is relative to the top of the viewport, not the buffer
COORD coordCursor;
// Left offset of the viewport, which may alter the horizontal position
SHORT viewportLeft;
// Line rendition of the current row, which can affect the cursor width
LineRendition lineRendition;
// For an underscore type _ cursor, how tall it should be as a % of cell height
ULONG ulCursorHeightPercent;

View file

@ -18,6 +18,7 @@ Author(s):
#include "Cluster.hpp"
#include "FontInfoDesired.hpp"
#include "IRenderData.hpp"
#include "../../buffer/out/LineRendition.hpp"
namespace Microsoft::Console::Render
{
@ -64,7 +65,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] virtual HRESULT ScrollFrame() noexcept = 0;
[[nodiscard]] virtual HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept = 0;
@ -75,6 +76,11 @@ namespace Microsoft::Console::Render
[[nodiscard]] virtual HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept = 0;
[[nodiscard]] virtual HRESULT ResetLineTransform() noexcept = 0;
[[nodiscard]] virtual HRESULT PrepareLineTransform(const LineRendition lineRendition,
const size_t targetRow,
const size_t viewportLeft) noexcept = 0;
[[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0;
[[nodiscard]] virtual HRESULT PaintBufferLine(gsl::span<const Cluster> const clusters,
const COORD coord,

View file

@ -40,6 +40,11 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override;
[[nodiscard]] HRESULT ResetLineTransform() noexcept override;
[[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
const size_t targetRow,
const size_t viewportLeft) noexcept override;
[[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept override;
void WaitUntilCanRender() noexcept override;

View file

@ -21,6 +21,7 @@ UiaEngine::UiaEngine(IUiaEventDispatcher* dispatcher) :
_cursorChanged{ false },
_isEnabled{ true },
_prevSelection{},
_prevCursorRegion{},
RenderEngineBase()
{
}
@ -66,18 +67,18 @@ UiaEngine::UiaEngine(IUiaEventDispatcher* dispatcher) :
// - Notifies us that the console has changed the position of the cursor.
// For UIA, this doesn't mean anything. So do nothing.
// Arguments:
// - pcoordCursor - the new position of the cursor
// - psrRegion - the region covered by the cursor
// Return Value:
// - S_OK
[[nodiscard]] HRESULT UiaEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept
[[nodiscard]] HRESULT UiaEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept
try
{
RETURN_HR_IF_NULL(E_INVALIDARG, pcoordCursor);
RETURN_HR_IF_NULL(E_INVALIDARG, psrRegion);
// check if cursor moved
if (*pcoordCursor != _prevCursorPos)
if (*psrRegion != _prevCursorRegion)
{
_prevCursorPos = *pcoordCursor;
_prevCursorRegion = *psrRegion;
_cursorChanged = true;
}
return S_OK;

View file

@ -43,7 +43,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT ScrollFrame() noexcept override;
[[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;
@ -86,6 +86,6 @@ namespace Microsoft::Console::Render
Microsoft::Console::Types::IUiaEventDispatcher* _dispatcher;
std::vector<SMALL_RECT> _prevSelection;
til::point _prevCursorPos;
SMALL_RECT _prevCursorRegion;
};
}

View file

@ -59,10 +59,10 @@ CATCH_RETURN();
// Routine Description:
// - Notifies us that the console has changed the position of the cursor.
// Arguments:
// - pcoordCursor - the new position of the cursor
// - psrRegion - the region covered by the cursor
// Return Value:
// - S_OK
[[nodiscard]] HRESULT VtEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept
[[nodiscard]] HRESULT VtEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept
{
// If we just inherited the cursor, we're going to get an InvalidateCursor
// for both where the old cursor was, and where the new cursor is
@ -70,9 +70,9 @@ CATCH_RETURN();
// We should ignore the first one, but after that, if the client application
// is moving the cursor around in the viewport, move our virtual top
// up to meet their changes.
if (!_skipCursor && _virtualTop > pcoordCursor->Y)
if (!_skipCursor && _virtualTop > psrRegion->Top)
{
_virtualTop = pcoordCursor->Y;
_virtualTop = psrRegion->Top;
}
_skipCursor = false;

View file

@ -49,7 +49,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] virtual HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept = 0;
[[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;
[[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override;
[[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;

View file

@ -166,7 +166,7 @@ bool WddmConEngine::IsInitialized()
return S_OK;
}
[[nodiscard]] HRESULT WddmConEngine::InvalidateCursor(const COORD* const /*pcoordCursor*/) noexcept
[[nodiscard]] HRESULT WddmConEngine::InvalidateCursor(const SMALL_RECT* const /*psrRegion*/) noexcept
{
return S_OK;
}

Some files were not shown because too many files have changed in this diff Show more