Merged PR 5704731: Migrate OSS up to e6aa90222
Related work items: MSFT-9667854, MSFT-31783834
This commit is contained in:
commit
ce99c2a349
3
.github/actions/spelling/dictionary/apis.txt
vendored
3
.github/actions/spelling/dictionary/apis.txt
vendored
|
@ -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
|
||||
|
|
6
.github/actions/spelling/expect/expect.txt
vendored
6
.github/actions/spelling/expect/expect.txt
vendored
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
},
|
||||
|
|
36
src/buffer/out/LineRendition.hpp
Normal file
36
src/buffer/out/LineRendition.hpp
Normal 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 };
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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=""
|
||||
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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; };
|
||||
|
|
|
@ -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>
|
|
@ -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") },
|
||||
};
|
||||
}();
|
||||
|
|
|
@ -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"";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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; };
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -53,6 +53,7 @@ namespace Microsoft.Terminal.Settings.Model
|
|||
TabSearch,
|
||||
MoveTab,
|
||||
BreakIntoDebugger,
|
||||
FindMatch,
|
||||
TogglePaneReadOnly
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 },
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"";
|
||||
|
||||
|
|
|
@ -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. I’ve
|
||||
// > 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<>
|
||||
|
|
50
src/inc/winrtTaefTemplates.hpp
Normal file
50
src/inc/winrtTaefTemplates.hpp
Normal 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. I’ve
|
||||
// > 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());
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
755
src/renderer/dx/DxFontRenderData.cpp
Normal file
755
src/renderer/dx/DxFontRenderData.cpp
Normal 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;
|
||||
}
|
96
src/renderer/dx/DxFontRenderData.h
Normal file
96
src/renderer/dx/DxFontRenderData.h
Normal 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;
|
||||
};
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -33,6 +33,7 @@ INCLUDES = \
|
|||
SOURCES = \
|
||||
$(SOURCES) \
|
||||
..\DxRenderer.cpp \
|
||||
..\DxFontRenderData.cpp \
|
||||
..\CustomTextRenderer.cpp \
|
||||
..\CustomTextLayout.cpp \
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue