Migrate OSS up to d3b9a780d
This commit is contained in:
commit
cabb83db61
3
.github/actions/spelling/allow/apis.txt
vendored
3
.github/actions/spelling/allow/apis.txt
vendored
|
@ -74,6 +74,7 @@ llu
|
|||
localtime
|
||||
lround
|
||||
LSHIFT
|
||||
mov
|
||||
msappx
|
||||
MULTIPLEUSE
|
||||
NCHITTEST
|
||||
|
@ -98,6 +99,7 @@ overridable
|
|||
PAGESCROLL
|
||||
PICKFOLDERS
|
||||
pmr
|
||||
rcx
|
||||
REGCLS
|
||||
RETURNCMD
|
||||
rfind
|
||||
|
@ -138,6 +140,7 @@ winmain
|
|||
wpc
|
||||
wsregex
|
||||
wwinmain
|
||||
xchg
|
||||
XDocument
|
||||
XElement
|
||||
xfacet
|
||||
|
|
3
.github/actions/spelling/allow/microsoft.txt
vendored
3
.github/actions/spelling/allow/microsoft.txt
vendored
|
@ -33,6 +33,7 @@ MSVC
|
|||
muxc
|
||||
netcore
|
||||
osgvsowi
|
||||
PFILETIME
|
||||
pgc
|
||||
pgo
|
||||
pgosweep
|
||||
|
@ -40,10 +41,12 @@ powerrename
|
|||
powershell
|
||||
propkey
|
||||
pscustomobject
|
||||
QWORD
|
||||
robocopy
|
||||
SACLs
|
||||
Shobjidl
|
||||
Skype
|
||||
SRW
|
||||
sxs
|
||||
Sysinternals
|
||||
sysnative
|
||||
|
|
3
.github/actions/spelling/expect/expect.txt
vendored
3
.github/actions/spelling/expect/expect.txt
vendored
|
@ -1443,6 +1443,7 @@ MSVCRTD
|
|||
MSVS
|
||||
msys
|
||||
msysgit
|
||||
MTSM
|
||||
mui
|
||||
Mul
|
||||
multiline
|
||||
|
@ -2416,7 +2417,6 @@ uapadmin
|
|||
UAX
|
||||
ubuntu
|
||||
ucd
|
||||
ucd
|
||||
ucdxml
|
||||
uch
|
||||
UCHAR
|
||||
|
@ -2780,7 +2780,6 @@ xml
|
|||
xmlns
|
||||
xor
|
||||
xorg
|
||||
xorg
|
||||
Xpath
|
||||
XPosition
|
||||
XResource
|
||||
|
|
|
@ -6,9 +6,9 @@ Adding a setting to Windows Terminal is fairly straightforward. This guide serve
|
|||
|
||||
The Terminal Settings Model (`Microsoft.Terminal.Settings.Model`) is responsible for (de)serializing and exposing settings.
|
||||
|
||||
### `GETSET_SETTING` macro
|
||||
### `INHERITABLE_SETTING` macro
|
||||
|
||||
The `GETSET_SETTING` macro can be used to implement inheritance for your new setting and store the setting in the settings model. It takes three parameters:
|
||||
The `INHERITABLE_SETTING` macro can be used to implement inheritance for your new setting and store the setting in the settings model. It takes three parameters:
|
||||
- `type`: the type that the setting will be stored as
|
||||
- `name`: the name of the variable for storage
|
||||
- `defaultValue`: the value to use if the user does not define the setting anywhere
|
||||
|
@ -20,7 +20,7 @@ This tutorial will add `CloseOnExitMode CloseOnExit` as a profile setting.
|
|||
1. In `Profile.h`, declare/define the setting:
|
||||
|
||||
```c++
|
||||
GETSET_SETTING(CloseOnExitMode, CloseOnExit, CloseOnExitMode::Graceful)
|
||||
INHERITABLE_SETTING(CloseOnExitMode, CloseOnExit, CloseOnExitMode::Graceful)
|
||||
```
|
||||
|
||||
2. In `Profile.idl`, expose the setting via WinRT:
|
||||
|
@ -141,7 +141,7 @@ struct OpenSettingsArgs : public OpenSettingsArgsT<OpenSettingsArgs>
|
|||
OpenSettingsArgs() = default;
|
||||
|
||||
// adds a getter/setter for your argument, and defines the json key
|
||||
GETSET_PROPERTY(SettingsTarget, Target, SettingsTarget::SettingsFile);
|
||||
WINRT_PROPERTY(SettingsTarget, Target, SettingsTarget::SettingsFile);
|
||||
static constexpr std::string_view TargetKey{ "target" };
|
||||
|
||||
public:
|
||||
|
@ -213,9 +213,9 @@ Terminal-level settings are settings that affect a shell session. Generally, the
|
|||
- Declare the setting in `IControlSettings.idl` or `ICoreSettings.idl` (whichever is relevant to your setting). If your setting is an enum setting, declare the enum here instead of in the `TerminalSettingsModel` project.
|
||||
- In `TerminalSettings.h`, declare/define the setting...
|
||||
```c++
|
||||
// The GETSET_PROPERTY macro declares/defines a getter setter for the setting.
|
||||
// Like GETSET_SETTING, it takes in a type, name, and defaultValue.
|
||||
GETSET_PROPERTY(bool, UseAcrylic, false);
|
||||
// The WINRT_PROPERTY macro declares/defines a getter setter for the setting.
|
||||
// Like INHERITABLE_SETTING, it takes in a type, name, and defaultValue.
|
||||
WINRT_PROPERTY(bool, UseAcrylic, false);
|
||||
```
|
||||
- In `TerminalSettings.cpp`...
|
||||
- update `_ApplyProfileSettings` for profile settings
|
||||
|
|
|
@ -165,6 +165,49 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FontConfig": {
|
||||
"properties": {
|
||||
"face": {
|
||||
"default": "Cascadia Mono",
|
||||
"description": "Name of the font face used in the profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"default": 12,
|
||||
"description": "Size of the font in points.",
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"weight": {
|
||||
"default": "normal",
|
||||
"description": "Sets the weight (lightness or heaviness of the strokes) for the given font. Possible values:\n -\"thin\"\n -\"extra-light\"\n -\"light\"\n -\"semi-light\"\n -\"normal\" (default)\n -\"medium\"\n -\"semi-bold\"\n -\"bold\"\n -\"extra-bold\"\n -\"black\"\n -\"extra-black\"\n or the corresponding numeric representation of OpenType font weight.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"thin",
|
||||
"extra-light",
|
||||
"light",
|
||||
"semi-light",
|
||||
"normal",
|
||||
"medium",
|
||||
"semi-bold",
|
||||
"bold",
|
||||
"extra-bold",
|
||||
"black",
|
||||
"extra-black"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"maximum": 990,
|
||||
"minimum": 100,
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ProfileGuid": {
|
||||
"default": "{}",
|
||||
"pattern": "^\\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\\}$",
|
||||
|
@ -546,12 +589,14 @@
|
|||
"action": { "type": "string", "pattern": "openSettings" },
|
||||
"target": {
|
||||
"type": "string",
|
||||
"default": "settingsFile",
|
||||
"description": "The settings file to open.",
|
||||
"default": "settingsUI",
|
||||
"description": "Opens Settings UI or settings file.",
|
||||
"enum": [
|
||||
"settingsFile",
|
||||
"defaultsFile",
|
||||
"allFiles"
|
||||
"allFiles",
|
||||
"settingsUI"
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -646,6 +691,25 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CloseTabAction": {
|
||||
"description": "Arguments for a closeTab action",
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/ShortcutAction" },
|
||||
{
|
||||
"properties": {
|
||||
"action": { "type": "string", "pattern": "closeTab" },
|
||||
"index": {
|
||||
"oneOf": [
|
||||
{ "type": "integer" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"default": null,
|
||||
"description": "Close the tab at this index. If no index is provided, use the focused tab's index."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ScrollUpAction": {
|
||||
"description": "Arguments for a scrollUp action",
|
||||
"allOf": [
|
||||
|
@ -897,6 +961,7 @@
|
|||
{ "$ref": "#/definitions/WtAction" },
|
||||
{ "$ref": "#/definitions/CloseOtherTabsAction" },
|
||||
{ "$ref": "#/definitions/CloseTabsAfterAction" },
|
||||
{ "$ref": "#/definitions/CloseTabAction" },
|
||||
{ "$ref": "#/definitions/ScrollUpAction" },
|
||||
{ "$ref": "#/definitions/ScrollDownAction" },
|
||||
{ "$ref": "#/definitions/MoveTabAction" },
|
||||
|
@ -1242,6 +1307,11 @@
|
|||
"description": "Sets the appearance of the terminal when it is unfocused.",
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"font": {
|
||||
"$ref": "#/definitions/FontConfig",
|
||||
"description": "Sets the font options of the terminal.",
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"backgroundImage": {
|
||||
"description": "Sets the file location of the image to draw over the window background.",
|
||||
"oneOf": [
|
||||
|
@ -1358,18 +1428,20 @@
|
|||
},
|
||||
"fontFace": {
|
||||
"default": "Cascadia Mono",
|
||||
"description": "Name of the font face used in the profile.",
|
||||
"type": "string"
|
||||
"description": "[deprecated] Define 'face' within the 'font' object instead.",
|
||||
"type": "string",
|
||||
"deprecated": true
|
||||
},
|
||||
"fontSize": {
|
||||
"default": 12,
|
||||
"description": "Size of the font in points.",
|
||||
"description": "[deprecated] Define 'size' within the 'font' object instead.",
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"deprecated": true
|
||||
},
|
||||
"fontWeight": {
|
||||
"default": "normal",
|
||||
"description": "Sets the weight (lightness or heaviness of the strokes) for the given font. Possible values:\n -\"thin\"\n -\"extra-light\"\n -\"light\"\n -\"semi-light\"\n -\"normal\" (default)\n -\"medium\"\n -\"semi-bold\"\n -\"bold\"\n -\"extra-bold\"\n -\"black\"\n -\"extra-black\"\n or the corresponding numeric representation of OpenType font weight.",
|
||||
"description": "[deprecated] Define 'weight' within the 'font' object instead.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
|
@ -1392,7 +1464,8 @@
|
|||
"minimum": 100,
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
],
|
||||
"deprecated": true
|
||||
},
|
||||
"foreground": {
|
||||
"$ref": "#/definitions/Color",
|
||||
|
|
|
@ -28,7 +28,7 @@ Below is the schedule for when milestones will be included in release builds of
|
|||
| 2021-01-31 | [1.6] in Windows Terminal Preview<br>[1.5] in Windows Terminal | [Windows Terminal Preview 1.6 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-6-release/) |
|
||||
| 2021-03-01 | [1.7] in Windows Terminal Preview<br>[1.6] in Windows Terminal | [Windows Terminal Preview 1.7 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-7-release/) |
|
||||
| 2021-04-14 | [1.8] in Windows Terminal Preview<br>[1.7] in Windows Terminal | [Windows Terminal Preview 1.8 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-8-release/) |
|
||||
| 2021-05-31 | [1.9] in Windows Terminal Preview<br>[1.8] in Windows Terminal | |
|
||||
| 2021-05-31 | [1.9] in Windows Terminal Preview<br>[1.8] in Windows Terminal | [Windows Terminal Preview 1.9 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-9-release/) |
|
||||
| 2021-07-31 | 1.10 in Windows Terminal Preview<br>[1.9] in Windows Terminal | |
|
||||
| 2021-08-30 | 1.11 in Windows Terminal Preview<br>1.10 in Windows Terminal | |
|
||||
| 2021-10-31 | 1.12 in Windows Terminal Preview<br>1.11 in Windows Terminal | |
|
||||
|
|
|
@ -9,30 +9,22 @@ namespace winrt::SampleApp::implementation
|
|||
|
||||
IXamlType GetXamlType(::winrt::Windows::UI::Xaml::Interop::TypeName const& type)
|
||||
{
|
||||
return AppProvider()->GetXamlType(type);
|
||||
return _appProvider.GetXamlType(type);
|
||||
}
|
||||
|
||||
IXamlType GetXamlType(::winrt::hstring const& fullName)
|
||||
{
|
||||
return AppProvider()->GetXamlType(fullName);
|
||||
return _appProvider.GetXamlType(fullName);
|
||||
}
|
||||
|
||||
::winrt::com_array<::winrt::Windows::UI::Xaml::Markup::XmlnsDefinition> GetXmlnsDefinitions()
|
||||
{
|
||||
return AppProvider()->GetXmlnsDefinitions();
|
||||
return _appProvider.GetXmlnsDefinitions();
|
||||
}
|
||||
|
||||
private:
|
||||
bool _contentLoaded{ false };
|
||||
std::shared_ptr<XamlMetaDataProvider> _appProvider;
|
||||
std::shared_ptr<XamlMetaDataProvider> AppProvider()
|
||||
{
|
||||
if (!_appProvider)
|
||||
{
|
||||
_appProvider = std::make_shared<XamlMetaDataProvider>();
|
||||
}
|
||||
return _appProvider;
|
||||
}
|
||||
winrt::SampleApp::XamlMetaDataProvider _appProvider;
|
||||
};
|
||||
|
||||
template<typename D, typename... I>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<!-- Windows 10 1903 -->
|
||||
<!-- See https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/xaml-islands -->
|
||||
<!-- "maxversiontested" is CASE SENSITIVE. Do not change this.-->
|
||||
<maxversiontested Id="10.0.19041.0"/>
|
||||
<maxversiontested Id="10.0.18362.0"/>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
|
|
@ -41,6 +41,7 @@ namespace SettingsModelLocalTests
|
|||
TEST_METHOD(ColorScheme);
|
||||
TEST_METHOD(Actions);
|
||||
TEST_METHOD(CascadiaSettings);
|
||||
TEST_METHOD(LegacyFontSettings);
|
||||
|
||||
TEST_CLASS_SETUP(ClassSetup)
|
||||
{
|
||||
|
@ -138,9 +139,11 @@ namespace SettingsModelLocalTests
|
|||
"tabTitle": "Cool Tab",
|
||||
"suppressApplicationTitle": false,
|
||||
|
||||
"fontFace": "Cascadia Mono",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal",
|
||||
"font": {
|
||||
"face": "Cascadia Mono",
|
||||
"size": 12,
|
||||
"weight": "normal"
|
||||
},
|
||||
"padding": "8, 8, 8, 8",
|
||||
"antialiasingMode": "grayscale",
|
||||
|
||||
|
@ -402,11 +405,13 @@ namespace SettingsModelLocalTests
|
|||
|
||||
"profiles": {
|
||||
"defaults": {
|
||||
"fontFace": "Zamora Code"
|
||||
"font": {
|
||||
"face": "Zamora Code"
|
||||
}
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"fontFace": "Cascadia Code",
|
||||
"font": { "face": "Cascadia Code" },
|
||||
"guid": "{61c54bbd-1111-5271-96e7-009a87ff44bf}",
|
||||
"name": "HowettShell"
|
||||
},
|
||||
|
@ -464,4 +469,35 @@ namespace SettingsModelLocalTests
|
|||
const auto result{ settings->ToJson() };
|
||||
VERIFY_ARE_EQUAL(toString(settings->_userSettings), toString(result));
|
||||
}
|
||||
|
||||
void SerializationTests::LegacyFontSettings()
|
||||
{
|
||||
const std::string profileString{ R"(
|
||||
{
|
||||
"name": "Profile with legacy font settings",
|
||||
|
||||
"fontFace": "Cascadia Mono",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal"
|
||||
})" };
|
||||
|
||||
const std::string expectedOutput{ R"(
|
||||
{
|
||||
"name": "Profile with legacy font settings",
|
||||
|
||||
"font": {
|
||||
"face": "Cascadia Mono",
|
||||
"size": 12,
|
||||
"weight": "normal"
|
||||
}
|
||||
})" };
|
||||
|
||||
const auto json{ VerifyParseSucceeded(profileString) };
|
||||
const auto settings{ implementation::Profile::FromJson(json) };
|
||||
const auto result{ settings->ToJson() };
|
||||
|
||||
const auto jsonOutput{ VerifyParseSucceeded(expectedOutput) };
|
||||
|
||||
VERIFY_ARE_EQUAL(toString(jsonOutput), toString(result));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,26 @@ namespace winrt::TerminalApp::implementation
|
|||
void TerminalPage::_HandleCloseTab(const IInspectable& /*sender*/,
|
||||
const ActionEventArgs& args)
|
||||
{
|
||||
_CloseFocusedTab();
|
||||
args.Handled(true);
|
||||
if (const auto realArgs = args.ActionArgs().try_as<CloseTabArgs>())
|
||||
{
|
||||
uint32_t index;
|
||||
if (realArgs.Index())
|
||||
{
|
||||
index = realArgs.Index().Value();
|
||||
}
|
||||
else if (auto focusedTabIndex = _GetFocusedTabIndex())
|
||||
{
|
||||
index = *focusedTabIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Handled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_CloseTabAtIndex(index);
|
||||
args.Handled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalPage::_HandleClosePane(const IInspectable& /*sender*/,
|
||||
|
|
|
@ -189,9 +189,7 @@ namespace winrt::TerminalApp::implementation
|
|||
}
|
||||
|
||||
AppLogic::AppLogic() :
|
||||
_dialogLock{},
|
||||
_loadedInitialSettings{ false },
|
||||
_settingsLoadedResult{ S_OK }
|
||||
_reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); } }
|
||||
{
|
||||
// For your own sanity, it's better to do setup outside the ctor.
|
||||
// If you do any setup in the ctor that ends up throwing an exception,
|
||||
|
@ -204,6 +202,13 @@ namespace winrt::TerminalApp::implementation
|
|||
// SetTitleBarContent
|
||||
_isElevated = _isUserAdmin();
|
||||
_root = winrt::make_self<TerminalPage>();
|
||||
|
||||
_reloadSettings = std::make_shared<ThrottledFuncTrailing<>>(_root->Dispatcher(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() {
|
||||
if (auto self{ weakSelf.get() })
|
||||
{
|
||||
self->_ReloadSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -859,59 +864,29 @@ namespace winrt::TerminalApp::implementation
|
|||
// - <none>
|
||||
void AppLogic::_RegisterSettingsChange()
|
||||
{
|
||||
// Get the containing folder.
|
||||
const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } };
|
||||
const auto folder = settingsPath.parent_path();
|
||||
const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } };
|
||||
|
||||
_reader.create(folder.c_str(),
|
||||
false,
|
||||
wil::FolderChangeEvents::All,
|
||||
[this, settingsPath](wil::FolderChangeEvent event, PCWSTR fileModified) {
|
||||
// We want file modifications, AND when files are renamed to be
|
||||
// settings.json. This second case will oftentimes happen with text
|
||||
// editors, who will write a temp file, then rename it to be the
|
||||
// actual file you wrote. So listen for that too.
|
||||
if (!(event == wil::FolderChangeEvent::Modified ||
|
||||
event == wil::FolderChangeEvent::RenameNewName ||
|
||||
event == wil::FolderChangeEvent::Removed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_reader.create(
|
||||
settingsPath.parent_path().c_str(),
|
||||
false,
|
||||
// We want file modifications, AND when files are renamed to be
|
||||
// settings.json. This second case will oftentimes happen with text
|
||||
// editors, who will write a temp file, then rename it to be the
|
||||
// actual file you wrote. So listen for that too.
|
||||
wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime,
|
||||
[this, settingsBasename = settingsPath.filename(), stateBasename = statePath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) {
|
||||
const auto modifiedBasename = std::filesystem::path{ fileModified }.filename();
|
||||
|
||||
std::filesystem::path modifiedFilePath = fileModified;
|
||||
|
||||
// Getting basename (filename.ext)
|
||||
const auto settingsBasename = settingsPath.filename();
|
||||
const auto modifiedBasename = modifiedFilePath.filename();
|
||||
|
||||
if (settingsBasename == modifiedBasename)
|
||||
{
|
||||
this->_DispatchReloadSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Dispatches a settings reload with debounce.
|
||||
// Text editors implement Save in a bunch of different ways, so
|
||||
// this stops us from reloading too many times or too quickly.
|
||||
fire_and_forget AppLogic::_DispatchReloadSettings()
|
||||
{
|
||||
if (_settingsReloadQueued.exchange(true))
|
||||
{
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto weakSelf = get_weak();
|
||||
|
||||
co_await winrt::resume_after(std::chrono::milliseconds(100));
|
||||
co_await winrt::resume_foreground(_root->Dispatcher());
|
||||
|
||||
if (auto self{ weakSelf.get() })
|
||||
{
|
||||
_ReloadSettings();
|
||||
_settingsReloadQueued.store(false);
|
||||
}
|
||||
if (modifiedBasename == settingsBasename)
|
||||
{
|
||||
_reloadSettings->Run();
|
||||
}
|
||||
else if (modifiedBasename == stateBasename)
|
||||
{
|
||||
_reloadState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AppLogic::_ApplyLanguageSettingChange() noexcept
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
#include "FindTargetWindowResult.g.h"
|
||||
#include "TerminalPage.h"
|
||||
#include "Jumplist.h"
|
||||
#include "../../cascadia/inc/cppwinrt_utils.h"
|
||||
|
||||
#include <inc/cppwinrt_utils.h>
|
||||
#include <ThrottledFunc.h>
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
// fwdecl unittest classes
|
||||
|
@ -111,17 +113,15 @@ namespace winrt::TerminalApp::implementation
|
|||
|
||||
Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr };
|
||||
|
||||
HRESULT _settingsLoadedResult;
|
||||
winrt::hstring _settingsLoadExceptionText{};
|
||||
|
||||
bool _loadedInitialSettings;
|
||||
|
||||
wil::unique_folder_change_reader_nothrow _reader;
|
||||
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
|
||||
til::throttled_func_trailing<> _reloadState;
|
||||
winrt::hstring _settingsLoadExceptionText;
|
||||
HRESULT _settingsLoadedResult = S_OK;
|
||||
bool _loadedInitialSettings = false;
|
||||
|
||||
std::shared_mutex _dialogLock;
|
||||
|
||||
std::atomic<bool> _settingsReloadQueued{ false };
|
||||
|
||||
::TerminalApp::AppCommandlineArgs _appArgs;
|
||||
::TerminalApp::AppCommandlineArgs _settingsAppArgs;
|
||||
static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view<const hstring> args,
|
||||
|
|
|
@ -617,17 +617,6 @@ namespace winrt::TerminalApp::implementation
|
|||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Close the currently focused tab. Focus will move to the left, if possible.
|
||||
void TerminalPage::_CloseFocusedTab()
|
||||
{
|
||||
if (auto index{ _GetFocusedTabIndex() })
|
||||
{
|
||||
auto tab{ _tabs.GetAt(*index) };
|
||||
_HandleCloseTabRequested(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Close the currently focused pane. If the pane is the last pane in the
|
||||
// tab, the tab will also be closed. This will happen when we handle the
|
||||
|
@ -675,6 +664,20 @@ namespace winrt::TerminalApp::implementation
|
|||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Close the tab at the given index.
|
||||
void TerminalPage::_CloseTabAtIndex(uint32_t index)
|
||||
{
|
||||
if (index >= _tabs.Size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (auto tab{ _tabs.GetAt(index) })
|
||||
{
|
||||
_HandleCloseTabRequested(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Closes provided tabs one by one
|
||||
// Arguments:
|
||||
|
|
|
@ -216,6 +216,7 @@ namespace winrt::TerminalApp::implementation
|
|||
void _DuplicateTab(const TerminalTab& tab);
|
||||
|
||||
winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::TabBase tab);
|
||||
void _CloseTabAtIndex(uint32_t index);
|
||||
void _RemoveTab(const winrt::TerminalApp::TabBase& tab);
|
||||
winrt::fire_and_forget _RemoveTabs(const std::vector<winrt::TerminalApp::TabBase> tabs);
|
||||
|
||||
|
@ -238,7 +239,6 @@ namespace winrt::TerminalApp::implementation
|
|||
TerminalApp::TabBase _GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept;
|
||||
|
||||
winrt::fire_and_forget _SetFocusedTab(const winrt::TerminalApp::TabBase tab);
|
||||
void _CloseFocusedTab();
|
||||
winrt::fire_and_forget _CloseFocusedPane();
|
||||
|
||||
winrt::fire_and_forget _RemoveOnCloseRoutine(Microsoft::UI::Xaml::Controls::TabViewItem tabViewItem, winrt::com_ptr<TerminalPage> page);
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
#include "../../cascadia/TerminalCore/Terminal.hpp"
|
||||
#include "../buffer/out/search.h"
|
||||
#include "cppwinrt_utils.h"
|
||||
#include "ThrottledFunc.h"
|
||||
|
||||
namespace ControlUnitTests
|
||||
{
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
#include "../buffer/out/search.h"
|
||||
#include "cppwinrt_utils.h"
|
||||
#include "SearchBoxControl.h"
|
||||
#include "ThrottledFunc.h"
|
||||
|
||||
#include "ControlInteractivity.h"
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
<ClInclude Include="TermControlAutomationPeer.h">
|
||||
<DependentUpon>TermControlAutomationPeer.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ThrottledFunc.h" />
|
||||
<ClInclude Include="TSFInputControl.h">
|
||||
<DependentUpon>TSFInputControl.xaml</DependentUpon>
|
||||
</ClInclude>
|
||||
|
|
|
@ -1,186 +0,0 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- ThrottledFunc.h
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
#include "pch.h"
|
||||
|
||||
template<typename... Args>
|
||||
class ThrottledFuncStorage
|
||||
{
|
||||
public:
|
||||
template<typename... MakeArgs>
|
||||
bool Emplace(MakeArgs&&... args)
|
||||
{
|
||||
std::scoped_lock guard{ _lock };
|
||||
|
||||
const bool hadValue = _pendingRunArgs.has_value();
|
||||
_pendingRunArgs.emplace(std::forward<MakeArgs>(args)...);
|
||||
return hadValue;
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void ModifyPending(F f)
|
||||
{
|
||||
std::scoped_lock guard{ _lock };
|
||||
|
||||
if (_pendingRunArgs.has_value())
|
||||
{
|
||||
std::apply(f, _pendingRunArgs.value());
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<Args...> Extract()
|
||||
{
|
||||
decltype(_pendingRunArgs) args;
|
||||
std::scoped_lock guard{ _lock };
|
||||
_pendingRunArgs.swap(args);
|
||||
return args.value();
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex _lock;
|
||||
std::optional<std::tuple<Args...>> _pendingRunArgs;
|
||||
};
|
||||
|
||||
template<>
|
||||
class ThrottledFuncStorage<>
|
||||
{
|
||||
public:
|
||||
bool Emplace()
|
||||
{
|
||||
return _isRunPending.test_and_set(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::tuple<> Extract()
|
||||
{
|
||||
Reset();
|
||||
return {};
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
_isRunPending.clear(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic_flag _isRunPending;
|
||||
};
|
||||
|
||||
// Class Description:
|
||||
// - Represents a function that takes arguments and whose invocation is
|
||||
// delayed by a specified duration and rate-limited such that if the code
|
||||
// tries to run the function while a call to the function is already
|
||||
// pending, then the previous call with the previous arguments will be
|
||||
// cancelled and the call will be made with the new arguments instead.
|
||||
// - The function will be run on the the specified dispatcher.
|
||||
template<bool leading, typename... Args>
|
||||
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<leading, Args...>>
|
||||
{
|
||||
public:
|
||||
using Func = std::function<void(Args...)>;
|
||||
|
||||
ThrottledFunc(winrt::Windows::UI::Core::CoreDispatcher dispatcher, winrt::Windows::Foundation::TimeSpan delay, Func func) :
|
||||
_dispatcher{ std::move(dispatcher) },
|
||||
_delay{ std::move(delay) },
|
||||
_func{ std::move(func) }
|
||||
{
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Runs the function later with the specified arguments, except if `Run`
|
||||
// is called again before with new arguments, in which case the new
|
||||
// arguments will be used instead.
|
||||
// - For more information, read the class' documentation.
|
||||
// - This method is always thread-safe. It can be called multiple times on
|
||||
// different threads.
|
||||
// Arguments:
|
||||
// - args: the arguments to pass to the function
|
||||
// Return Value:
|
||||
// - <none>
|
||||
template<typename... MakeArgs>
|
||||
void Run(MakeArgs&&... args)
|
||||
{
|
||||
if (!_storage.Emplace(std::forward<MakeArgs>(args)...))
|
||||
{
|
||||
_Fire();
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Modifies the pending arguments for the next function invocation, if
|
||||
// there is one pending currently.
|
||||
// - Let's say that you just called the `Run` method with some arguments.
|
||||
// After the delay specified in the constructor, the function specified
|
||||
// in the constructor will be called with these arguments.
|
||||
// - By using this method, you can modify the arguments before the function
|
||||
// is called.
|
||||
// - You pass a function to this method which will take references to
|
||||
// the arguments (one argument corresponds to one reference to an
|
||||
// argument) and will modify them.
|
||||
// - When there is no pending invocation of the function, this method will
|
||||
// not do anything.
|
||||
// - This method is always thread-safe. It can be called multiple times on
|
||||
// different threads.
|
||||
// Arguments:
|
||||
// - f: the function to call with references to the arguments
|
||||
// Return Value:
|
||||
// - <none>
|
||||
template<typename F>
|
||||
void ModifyPending(F f)
|
||||
{
|
||||
_storage.ModifyPending(f);
|
||||
}
|
||||
|
||||
private:
|
||||
winrt::fire_and_forget _Fire()
|
||||
{
|
||||
const auto dispatcher = _dispatcher;
|
||||
auto weakSelf = this->weak_from_this();
|
||||
|
||||
if constexpr (leading)
|
||||
{
|
||||
co_await winrt::resume_foreground(dispatcher);
|
||||
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
self->_func();
|
||||
}
|
||||
else
|
||||
{
|
||||
co_return;
|
||||
}
|
||||
|
||||
co_await winrt::resume_after(_delay);
|
||||
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
self->_storage.Reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
co_await winrt::resume_after(_delay);
|
||||
co_await winrt::resume_foreground(dispatcher);
|
||||
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
std::apply(self->_func, self->_storage.Extract());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Core::CoreDispatcher _dispatcher;
|
||||
winrt::Windows::Foundation::TimeSpan _delay;
|
||||
Func _func;
|
||||
|
||||
ThrottledFuncStorage<Args...> _storage;
|
||||
};
|
||||
|
||||
template<typename... Args>
|
||||
using ThrottledFuncTrailing = ThrottledFunc<false, Args...>;
|
||||
using ThrottledFuncLeading = ThrottledFunc<true>;
|
|
@ -58,3 +58,5 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalControlProvider);
|
|||
#include <telemetry/ProjectTelemetry.h>
|
||||
|
||||
#include "til.h"
|
||||
|
||||
#include "ThrottledFunc.h"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "KeyBindingViewModel.g.cpp"
|
||||
#include "ActionsPageNavigationState.g.cpp"
|
||||
#include "LibraryResources.h"
|
||||
#include "../TerminalSettingsModel/AllShortcutActions.h"
|
||||
|
||||
using namespace winrt::Windows::Foundation;
|
||||
using namespace winrt::Windows::Foundation::Collections;
|
||||
|
@ -20,10 +21,12 @@ using namespace winrt::Microsoft::Terminal::Settings::Model;
|
|||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
{
|
||||
KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const Model::Command& cmd) :
|
||||
KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector<hstring>& availableActions) :
|
||||
_Keys{ keys },
|
||||
_KeyChordText{ Model::KeyChordSerialization::ToString(keys) },
|
||||
_Command{ cmd }
|
||||
_CurrentAction{ actionName },
|
||||
_ProposedAction{ box_value(actionName) },
|
||||
_AvailableActions{ availableActions }
|
||||
{
|
||||
// Add a property changed handler to our own property changed event.
|
||||
// This propagates changes from the settings model to anybody listening to our
|
||||
|
@ -43,6 +46,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
{
|
||||
_NotifyChanges(L"ShowEditButton");
|
||||
}
|
||||
else if (viewModelProperty == L"CurrentAction")
|
||||
{
|
||||
_NotifyChanges(L"Name");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -63,8 +70,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
if (_IsInEditMode)
|
||||
{
|
||||
// if we're in edit mode,
|
||||
// pre-populate the text box with the current keys
|
||||
// - pre-populate the text box with the current keys
|
||||
// - reset the combo box with the current action
|
||||
ProposedKeys(KeyChordText());
|
||||
ProposedAction(box_value(CurrentAction()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,13 +84,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
|
||||
void KeyBindingViewModel::AttemptAcceptChanges(hstring newKeyChordText)
|
||||
{
|
||||
auto args{ make_self<RebindKeysEventArgs>(_Keys, _Keys) };
|
||||
auto args{ make_self<ModifyKeyBindingEventArgs>(_Keys, _Keys, _CurrentAction, unbox_value<hstring>(_ProposedAction)) };
|
||||
|
||||
// Key Chord Text
|
||||
try
|
||||
{
|
||||
// Attempt to convert the provided key chord text
|
||||
const auto newKeyChord{ KeyChordSerialization::FromString(newKeyChordText) };
|
||||
args->NewKeys(newKeyChord);
|
||||
_RebindKeysRequestedHandlers(*this, *args);
|
||||
}
|
||||
catch (hresult_invalid_argument)
|
||||
{
|
||||
|
@ -94,6 +104,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
// Alternatively, we want a full key chord editor/listener.
|
||||
// If we implement that, we won't need this validation or error message.
|
||||
}
|
||||
|
||||
_ModifyKeyBindingRequestedHandlers(*this, *args);
|
||||
}
|
||||
|
||||
Actions::Actions()
|
||||
|
@ -118,16 +130,28 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
{
|
||||
_State = e.Parameter().as<Editor::ActionsPageNavigationState>();
|
||||
|
||||
// Populate AvailableActionAndArgs
|
||||
_AvailableActionMap = single_threaded_map<hstring, Model::ActionAndArgs>();
|
||||
std::vector<hstring> availableActionAndArgs;
|
||||
for (const auto& [name, actionAndArgs] : _State.Settings().ActionMap().AvailableActions())
|
||||
{
|
||||
availableActionAndArgs.push_back(name);
|
||||
_AvailableActionMap.Insert(name, actionAndArgs);
|
||||
}
|
||||
std::sort(begin(availableActionAndArgs), end(availableActionAndArgs));
|
||||
_AvailableActionAndArgs = single_threaded_observable_vector(std::move(availableActionAndArgs));
|
||||
|
||||
// Convert the key bindings from our settings into a view model representation
|
||||
const auto& keyBindingMap{ _State.Settings().ActionMap().KeyBindings() };
|
||||
std::vector<Editor::KeyBindingViewModel> keyBindingList;
|
||||
keyBindingList.reserve(keyBindingMap.Size());
|
||||
for (const auto& [keys, cmd] : keyBindingMap)
|
||||
{
|
||||
auto container{ make_self<KeyBindingViewModel>(keys, cmd) };
|
||||
// convert the cmd into a KeyBindingViewModel
|
||||
auto container{ make_self<KeyBindingViewModel>(keys, cmd.Name(), _AvailableActionAndArgs) };
|
||||
container->PropertyChanged({ this, &Actions::_ViewModelPropertyChangedHandler });
|
||||
container->DeleteKeyBindingRequested({ this, &Actions::_ViewModelDeleteKeyBindingHandler });
|
||||
container->RebindKeysRequested({ this, &Actions::_ViewModelRebindKeysHandler });
|
||||
container->ModifyKeyBindingRequested({ this, &Actions::_ViewModelModifyKeyBindingHandler });
|
||||
container->IsAutomationPeerAttached(_AutomationPeerAttached);
|
||||
keyBindingList.push_back(*container);
|
||||
}
|
||||
|
@ -228,12 +252,42 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
}
|
||||
}
|
||||
|
||||
void Actions::_ViewModelRebindKeysHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::RebindKeysEventArgs& args)
|
||||
void Actions::_ViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args)
|
||||
{
|
||||
auto applyChangesToSettingsModel = [=]() {
|
||||
// If the key chord was changed,
|
||||
// update the settings model and view model appropriately
|
||||
if (args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
|
||||
{
|
||||
// update settings model
|
||||
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
|
||||
|
||||
// update view model
|
||||
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
|
||||
senderVMImpl->Keys(args.NewKeys());
|
||||
}
|
||||
|
||||
// If the action was changed,
|
||||
// update the settings model and view model appropriately
|
||||
if (args.OldActionName() != args.NewActionName())
|
||||
{
|
||||
// convert the action's name into a view model.
|
||||
const auto& newAction{ _AvailableActionMap.Lookup(args.NewActionName()) };
|
||||
|
||||
// update settings model
|
||||
_State.Settings().ActionMap().RegisterKeyBinding(args.NewKeys(), newAction);
|
||||
|
||||
// update view model
|
||||
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
|
||||
senderVMImpl->CurrentAction(args.NewActionName());
|
||||
}
|
||||
};
|
||||
|
||||
// Check for this special case:
|
||||
// we're changing the key chord,
|
||||
// but the new key chord is already in use
|
||||
if (args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
|
||||
{
|
||||
// We're actually changing the key chord
|
||||
const auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
|
||||
const auto& conflictingCmd{ _State.Settings().ActionMap().GetActionByKeyChord(args.NewKeys()) };
|
||||
if (conflictingCmd)
|
||||
{
|
||||
|
@ -262,8 +316,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
senderVM.AcceptChangesFlyout(nullptr);
|
||||
|
||||
// update settings model and view model
|
||||
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
|
||||
senderVMImpl->Keys(args.NewKeys());
|
||||
applyChangesToSettingsModel();
|
||||
senderVM.ToggleEditMode();
|
||||
});
|
||||
|
||||
|
@ -278,17 +331,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
senderVM.AcceptChangesFlyout(acceptChangesFlyout);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// update settings model
|
||||
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
|
||||
|
||||
// update view model (keys)
|
||||
senderVMImpl->Keys(args.NewKeys());
|
||||
}
|
||||
}
|
||||
|
||||
// update view model (exit edit mode)
|
||||
// update settings model and view model
|
||||
applyChangesToSettingsModel();
|
||||
|
||||
// We NEED to toggle the edit mode here,
|
||||
// so that if nothing changed, we still exit
|
||||
// edit mode.
|
||||
senderVM.ToggleEditMode();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
#include "Actions.g.h"
|
||||
#include "KeyBindingViewModel.g.h"
|
||||
#include "ActionsPageNavigationState.g.h"
|
||||
#include "RebindKeysEventArgs.g.h"
|
||||
#include "ModifyKeyBindingEventArgs.g.h"
|
||||
#include "Utils.h"
|
||||
#include "ViewModelHelpers.h"
|
||||
|
||||
|
@ -20,25 +20,28 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
}
|
||||
};
|
||||
|
||||
struct RebindKeysEventArgs : RebindKeysEventArgsT<RebindKeysEventArgs>
|
||||
struct ModifyKeyBindingEventArgs : ModifyKeyBindingEventArgsT<ModifyKeyBindingEventArgs>
|
||||
{
|
||||
public:
|
||||
RebindKeysEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys) :
|
||||
ModifyKeyBindingEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys, const hstring oldActionName, const hstring newActionName) :
|
||||
_OldKeys{ oldKeys },
|
||||
_NewKeys{ newKeys } {}
|
||||
_NewKeys{ newKeys },
|
||||
_OldActionName{ std::move(oldActionName) },
|
||||
_NewActionName{ std::move(newActionName) } {}
|
||||
|
||||
WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr);
|
||||
WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr);
|
||||
WINRT_PROPERTY(hstring, OldActionName);
|
||||
WINRT_PROPERTY(hstring, NewActionName);
|
||||
};
|
||||
|
||||
struct KeyBindingViewModel : KeyBindingViewModelT<KeyBindingViewModel>, ViewModelHelper<KeyBindingViewModel>
|
||||
{
|
||||
public:
|
||||
KeyBindingViewModel(const Control::KeyChord& keys, const Settings::Model::Command& cmd);
|
||||
KeyBindingViewModel(const Control::KeyChord& keys, const hstring& name, const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
|
||||
|
||||
hstring Name() const { return _Command.Name(); }
|
||||
hstring Name() const { return _CurrentAction; }
|
||||
hstring KeyChordText() const { return _KeyChordText; }
|
||||
Settings::Model::Command Command() const { return _Command; };
|
||||
|
||||
// UIA Text
|
||||
hstring EditButtonName() const noexcept;
|
||||
|
@ -59,20 +62,35 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
void AttemptAcceptChanges(hstring newKeyChordText);
|
||||
void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _Keys); }
|
||||
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false);
|
||||
// ProposedAction: the entry selected by the combo box; may disagree with the settings model.
|
||||
// CurrentAction: the combo box item that maps to the settings model value.
|
||||
// AvailableActions: the list of options in the combo box; both actions above must be in this list.
|
||||
// NOTE: ProposedAction and CurrentAction may disagree mainly due to the "edit mode" system in place.
|
||||
// Current Action serves as...
|
||||
// 1 - a record of what to set ProposedAction to on a cancellation
|
||||
// 2 - a form of translation between ProposedAction and the settings model
|
||||
// We would also need an ActionMap reference to remove this, but this is a better separation
|
||||
// of responsibilities.
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(IInspectable, ProposedAction);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction);
|
||||
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<hstring>, AvailableActions, nullptr);
|
||||
|
||||
// ProposedKeys: the text shown in the text box; may disagree with the settings model.
|
||||
// Keys: the key chord bound in the settings model.
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProposedKeys);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, Keys, nullptr);
|
||||
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Controls::Flyout, AcceptChangesFlyout, nullptr);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsAutomationPeerAttached, false);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsHovered, false);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsContainerFocused, false);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsEditButtonFocused, false);
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Media::Brush, ContainerBackground, nullptr);
|
||||
TYPED_EVENT(RebindKeysRequested, Editor::KeyBindingViewModel, Editor::RebindKeysEventArgs);
|
||||
TYPED_EVENT(ModifyKeyBindingRequested, Editor::KeyBindingViewModel, Editor::ModifyKeyBindingEventArgs);
|
||||
TYPED_EVENT(DeleteKeyBindingRequested, Editor::KeyBindingViewModel, Terminal::Control::KeyChord);
|
||||
|
||||
private:
|
||||
Settings::Model::Command _Command{ nullptr };
|
||||
hstring _KeyChordText{};
|
||||
};
|
||||
|
||||
|
@ -101,11 +119,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
private:
|
||||
void _ViewModelPropertyChangedHandler(const Windows::Foundation::IInspectable& senderVM, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
|
||||
void _ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& args);
|
||||
void _ViewModelRebindKeysHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::RebindKeysEventArgs& args);
|
||||
void _ViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args);
|
||||
|
||||
std::optional<uint32_t> _GetContainerIndexByKeyChord(const Control::KeyChord& keys);
|
||||
|
||||
bool _AutomationPeerAttached{ false };
|
||||
Windows::Foundation::Collections::IObservableVector<hstring> _AvailableActionAndArgs;
|
||||
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionMap;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,12 @@ import "EnumEntry.idl";
|
|||
|
||||
namespace Microsoft.Terminal.Settings.Editor
|
||||
{
|
||||
runtimeclass RebindKeysEventArgs
|
||||
runtimeclass ModifyKeyBindingEventArgs
|
||||
{
|
||||
Microsoft.Terminal.Control.KeyChord OldKeys { get; };
|
||||
Microsoft.Terminal.Control.KeyChord NewKeys { get; };
|
||||
String OldActionName { get; };
|
||||
String NewActionName { get; };
|
||||
}
|
||||
|
||||
runtimeclass KeyBindingViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
|
||||
|
@ -21,6 +23,7 @@ namespace Microsoft.Terminal.Settings.Editor
|
|||
Boolean ShowEditButton { get; };
|
||||
Boolean IsInEditMode { get; };
|
||||
String ProposedKeys;
|
||||
Object ProposedAction;
|
||||
Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout;
|
||||
String EditButtonName { get; };
|
||||
String CancelButtonName { get; };
|
||||
|
@ -34,11 +37,12 @@ namespace Microsoft.Terminal.Settings.Editor
|
|||
void ActionLostFocus();
|
||||
void EditButtonGettingFocus();
|
||||
void EditButtonLosingFocus();
|
||||
IObservableVector<String> AvailableActions { get; };
|
||||
void ToggleEditMode();
|
||||
void AttemptAcceptChanges();
|
||||
void DeleteKeyBinding();
|
||||
|
||||
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, RebindKeysEventArgs> RebindKeysRequested;
|
||||
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, ModifyKeyBindingEventArgs> ModifyKeyBindingRequested;
|
||||
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, Microsoft.Terminal.Control.KeyChord> DeleteKeyBindingRequested;
|
||||
}
|
||||
|
||||
|
|
|
@ -198,7 +198,15 @@
|
|||
<!-- Command Name -->
|
||||
<TextBlock Grid.Column="0"
|
||||
Style="{StaticResource KeyBindingNameTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
Text="{x:Bind Name, Mode=OneWay}"
|
||||
Visibility="{x:Bind IsInEditMode, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}" />
|
||||
|
||||
<!-- Edit Mode: Action Combo-box -->
|
||||
<ComboBox Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind AvailableActions, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ProposedAction, Mode=TwoWay}"
|
||||
Visibility="{x:Bind IsInEditMode, Mode=OneWay}" />
|
||||
|
||||
<!-- Key Chord Text -->
|
||||
<Border Grid.Column="1"
|
||||
|
|
|
@ -77,9 +77,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|||
OBSERVABLE_PROJECTED_SETTING(_profile, UseAcrylic);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, AcrylicOpacity);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, ScrollState);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, FontFace);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, FontSize);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, FontWeight);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile.FontInfo(), FontFace);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile.FontInfo(), FontSize);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile.FontInfo(), FontWeight);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, Padding);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, Commandline);
|
||||
OBSERVABLE_PROJECTED_SETTING(_profile, StartingDirectory);
|
||||
|
|
|
@ -216,7 +216,7 @@
|
|||
<comment>This is the header for a control that lets the user select the yellow color for text displayed on the screen.</comment>
|
||||
</data>
|
||||
<data name="Globals_Language.Header" xml:space="preserve">
|
||||
<value>Language</value>
|
||||
<value>Language (requires relaunch)</value>
|
||||
<comment>The header for a control allowing users to choose the app's language.</comment>
|
||||
</data>
|
||||
<data name="Globals_Language.HelpText" xml:space="preserve">
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "ActionAndArgs.g.cpp"
|
||||
|
||||
#include "JsonUtils.h"
|
||||
#include "HashUtils.h"
|
||||
|
||||
#include <LibraryResources.h>
|
||||
|
||||
|
@ -117,6 +118,35 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
#undef ON_ALL_ACTIONS_WITH_ARGS
|
||||
};
|
||||
|
||||
ActionAndArgs::ActionAndArgs(ShortcutAction action)
|
||||
{
|
||||
// Find the deserializer
|
||||
const auto deserializersIter = argSerializerMap.find(action);
|
||||
if (deserializersIter != argSerializerMap.end())
|
||||
{
|
||||
auto pfn = deserializersIter->second.first;
|
||||
if (pfn)
|
||||
{
|
||||
// Call the deserializer on an empty JSON object.
|
||||
// This ensures that we have a valid ActionArgs
|
||||
std::vector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> parseWarnings;
|
||||
std::tie(_Args, parseWarnings) = pfn({});
|
||||
}
|
||||
|
||||
// if an arg parser was registered, but failed,
|
||||
// return the invalid ActionAndArgs we started with.
|
||||
if (pfn && _Args == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Either...
|
||||
// (1) we don't have a deserializer, so it's ok for _Args to be null, or
|
||||
// (2) we had one AND it worked, so _Args is set up properly
|
||||
_Action = action;
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
// - Attempts to match a string to a ShortcutAction. If there's no match, then
|
||||
// returns ShortcutAction::Invalid
|
||||
|
@ -280,7 +310,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
{ ShortcutAction::AdjustFontSize, RS_(L"AdjustFontSizeCommandKey") },
|
||||
{ ShortcutAction::CloseOtherTabs, L"" }, // Intentionally omitted, must be generated by GenerateName
|
||||
{ ShortcutAction::ClosePane, RS_(L"ClosePaneCommandKey") },
|
||||
{ ShortcutAction::CloseTab, RS_(L"CloseTabCommandKey") },
|
||||
{ ShortcutAction::CloseTab, L"" }, // Intentionally omitted, must be generated by GenerateName
|
||||
{ ShortcutAction::CloseTabsAfter, L"" }, // Intentionally omitted, must be generated by GenerateName
|
||||
{ ShortcutAction::CloseWindow, RS_(L"CloseWindowCommandKey") },
|
||||
{ ShortcutAction::CopyText, RS_(L"CopyTextCommandKey") },
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
static Json::Value ToJson(const Model::ActionAndArgs& val);
|
||||
|
||||
ActionAndArgs() = default;
|
||||
ActionAndArgs(ShortcutAction action);
|
||||
ActionAndArgs(ShortcutAction action, IActionArgs args) :
|
||||
_Action{ action },
|
||||
_Args{ args } {};
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include "ExecuteCommandlineArgs.g.cpp"
|
||||
#include "CloseOtherTabsArgs.g.cpp"
|
||||
#include "CloseTabsAfterArgs.g.cpp"
|
||||
#include "CloseTabArgs.g.cpp"
|
||||
#include "MoveTabArgs.g.cpp"
|
||||
#include "FindMatchArgs.g.cpp"
|
||||
#include "ToggleCommandPaletteArgs.g.cpp"
|
||||
|
@ -469,6 +470,19 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
return RS_(L"CloseTabsAfterDefaultCommandKey");
|
||||
}
|
||||
|
||||
winrt::hstring CloseTabArgs::GenerateName() const
|
||||
{
|
||||
if (Index())
|
||||
{
|
||||
// "Close tab at index {0}"
|
||||
return winrt::hstring{
|
||||
fmt::format(std::wstring_view(RS_(L"CloseTabAtIndexCommandKey")),
|
||||
Index().Value())
|
||||
};
|
||||
}
|
||||
return RS_(L"CloseTabCommandKey");
|
||||
}
|
||||
|
||||
winrt::hstring ScrollUpArgs::GenerateName() const
|
||||
{
|
||||
if (RowsToScroll())
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include "ExecuteCommandlineArgs.g.h"
|
||||
#include "CloseOtherTabsArgs.g.h"
|
||||
#include "CloseTabsAfterArgs.g.h"
|
||||
#include "CloseTabArgs.g.h"
|
||||
#include "ScrollUpArgs.g.h"
|
||||
#include "ScrollDownArgs.g.h"
|
||||
#include "MoveTabArgs.g.h"
|
||||
|
@ -996,6 +997,57 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
}
|
||||
};
|
||||
|
||||
struct CloseTabArgs : public CloseTabArgsT<CloseTabArgs>
|
||||
{
|
||||
CloseTabArgs() = default;
|
||||
CloseTabArgs(uint32_t tabIndex) :
|
||||
_Index{ tabIndex } {};
|
||||
ACTION_ARG(Windows::Foundation::IReference<uint32_t>, Index, nullptr);
|
||||
|
||||
static constexpr std::string_view IndexKey{ "index" };
|
||||
|
||||
public:
|
||||
hstring GenerateName() const;
|
||||
|
||||
bool Equals(const IActionArgs& other)
|
||||
{
|
||||
auto otherAsUs = other.try_as<CloseTabArgs>();
|
||||
if (otherAsUs)
|
||||
{
|
||||
return otherAsUs->_Index == _Index;
|
||||
}
|
||||
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<CloseTabArgs>();
|
||||
JsonUtils::GetValueForKey(json, IndexKey, args->_Index);
|
||||
return { *args, {} };
|
||||
}
|
||||
static Json::Value ToJson(const IActionArgs& val)
|
||||
{
|
||||
if (!val)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
Json::Value json{ Json::ValueType::objectValue };
|
||||
const auto args{ get_self<CloseTabArgs>(val) };
|
||||
JsonUtils::SetValueForKey(json, IndexKey, args->_Index);
|
||||
return json;
|
||||
}
|
||||
IActionArgs Copy() const
|
||||
{
|
||||
auto copy{ winrt::make_self<CloseTabArgs>() };
|
||||
copy->_Index = _Index;
|
||||
return *copy;
|
||||
}
|
||||
size_t Hash() const
|
||||
{
|
||||
return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(Index());
|
||||
}
|
||||
};
|
||||
|
||||
struct MoveTabArgs : public MoveTabArgsT<MoveTabArgs>
|
||||
{
|
||||
MoveTabArgs() = default;
|
||||
|
@ -1600,6 +1652,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
|
|||
BASIC_FACTORY(ExecuteCommandlineArgs);
|
||||
BASIC_FACTORY(CloseOtherTabsArgs);
|
||||
BASIC_FACTORY(CloseTabsAfterArgs);
|
||||
BASIC_FACTORY(CloseTabArgs);
|
||||
BASIC_FACTORY(MoveTabArgs);
|
||||
BASIC_FACTORY(OpenSettingsArgs);
|
||||
BASIC_FACTORY(FindMatchArgs);
|
||||
|
|
|
@ -221,6 +221,12 @@ namespace Microsoft.Terminal.Settings.Model
|
|||
Windows.Foundation.IReference<UInt32> Index { get; };
|
||||
};
|
||||
|
||||
[default_interface] runtimeclass CloseTabArgs : IActionArgs
|
||||
{
|
||||
CloseTabArgs(UInt32 tabIndex);
|
||||
Windows.Foundation.IReference<UInt32> Index { get; };
|
||||
};
|
||||
|
||||
[default_interface] runtimeclass MoveTabArgs : IActionArgs
|
||||
{
|
||||
MoveTabArgs(MoveTabDirection direction);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "pch.h"
|
||||
#include "AllShortcutActions.h"
|
||||
#include "ActionMap.h"
|
||||
#include "AllShortcutActions.h"
|
||||
|
||||
#include "ActionMap.g.cpp"
|
||||
|
||||
|
@ -96,6 +97,75 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
return std::nullopt;
|
||||
}
|
||||
|
||||
static void RegisterShortcutAction(ShortcutAction shortcutAction, std::unordered_map<hstring, Model::ActionAndArgs>& list, std::unordered_set<InternalActionID>& visited)
|
||||
{
|
||||
const auto actionAndArgs{ make_self<ActionAndArgs>(shortcutAction) };
|
||||
if (actionAndArgs->Action() != ShortcutAction::Invalid)
|
||||
{
|
||||
/*We have a valid action.*/
|
||||
/*Check if the action was already added.*/
|
||||
if (visited.find(Hash(*actionAndArgs)) == visited.end())
|
||||
{
|
||||
/*This is an action that wasn't added!*/
|
||||
/*Let's add it.*/
|
||||
const auto name{ actionAndArgs->GenerateName() };
|
||||
list.insert({ name, *actionAndArgs });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Retrieves a map of actions that can be bound to a key
|
||||
Windows::Foundation::Collections::IMapView<hstring, Model::ActionAndArgs> ActionMap::AvailableActions()
|
||||
{
|
||||
if (!_AvailableActionsCache)
|
||||
{
|
||||
// populate _AvailableActionsCache
|
||||
std::unordered_map<hstring, Model::ActionAndArgs> availableActions;
|
||||
std::unordered_set<InternalActionID> visitedActionIDs;
|
||||
_PopulateAvailableActionsWithStandardCommands(availableActions, visitedActionIDs);
|
||||
|
||||
// now add any ShortcutActions that we might have missed
|
||||
#define ON_ALL_ACTIONS(action) RegisterShortcutAction(ShortcutAction::action, availableActions, visitedActionIDs);
|
||||
ALL_SHORTCUT_ACTIONS
|
||||
#undef ON_ALL_ACTIONS
|
||||
|
||||
_AvailableActionsCache = single_threaded_map<hstring, Model::ActionAndArgs>(std::move(availableActions));
|
||||
}
|
||||
return _AvailableActionsCache.GetView();
|
||||
}
|
||||
|
||||
void ActionMap::_PopulateAvailableActionsWithStandardCommands(std::unordered_map<hstring, Model::ActionAndArgs>& availableActions, std::unordered_set<InternalActionID>& visitedActionIDs) const
|
||||
{
|
||||
// Update AvailableActions and visitedActionIDs with our current layer
|
||||
for (const auto& [actionID, cmd] : _ActionMap)
|
||||
{
|
||||
if (cmd.ActionAndArgs().Action() != ShortcutAction::Invalid)
|
||||
{
|
||||
// Only populate AvailableActions with actions that haven't been visited already.
|
||||
if (visitedActionIDs.find(actionID) == visitedActionIDs.end())
|
||||
{
|
||||
const auto& name{ cmd.Name() };
|
||||
if (!name.empty())
|
||||
{
|
||||
// Update AvailableActions.
|
||||
const auto actionAndArgsImpl{ get_self<ActionAndArgs>(cmd.ActionAndArgs()) };
|
||||
availableActions.insert_or_assign(name, *actionAndArgsImpl->Copy());
|
||||
}
|
||||
|
||||
// Record that we already handled adding this action to the NameMap.
|
||||
visitedActionIDs.insert(actionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update NameMap and visitedActionIDs with our parents
|
||||
for (const auto& parent : _parents)
|
||||
{
|
||||
parent->_PopulateAvailableActionsWithStandardCommands(availableActions, visitedActionIDs);
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Retrieves a map of command names to the commands themselves
|
||||
// - These commands should not be modified directly because they may result in
|
||||
|
@ -164,25 +234,23 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
std::unordered_set<InternalActionID> visitedActionIDs;
|
||||
for (const auto& cmd : _GetCumulativeActions())
|
||||
{
|
||||
// skip over all invalid actions
|
||||
if (cmd.ActionAndArgs().Action() == ShortcutAction::Invalid)
|
||||
// only populate with valid commands
|
||||
if (cmd.ActionAndArgs().Action() != ShortcutAction::Invalid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only populate NameMap with actions that haven't been visited already.
|
||||
const auto actionID{ Hash(cmd.ActionAndArgs()) };
|
||||
if (visitedActionIDs.find(actionID) == visitedActionIDs.end())
|
||||
{
|
||||
const auto& name{ cmd.Name() };
|
||||
if (!name.empty())
|
||||
// Only populate NameMap with actions that haven't been visited already.
|
||||
const auto actionID{ Hash(cmd.ActionAndArgs()) };
|
||||
if (visitedActionIDs.find(actionID) == visitedActionIDs.end())
|
||||
{
|
||||
// Update NameMap.
|
||||
nameMap.insert_or_assign(name, cmd);
|
||||
}
|
||||
const auto& name{ cmd.Name() };
|
||||
if (!name.empty())
|
||||
{
|
||||
// Update NameMap.
|
||||
nameMap.insert_or_assign(name, cmd);
|
||||
}
|
||||
|
||||
// Record that we already handled adding this action to the NameMap.
|
||||
visitedActionIDs.emplace(actionID);
|
||||
// Record that we already handled adding this action to the NameMap.
|
||||
visitedActionIDs.emplace(actionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -759,4 +827,20 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
cmd->RegisterKey(keys);
|
||||
AddAction(*cmd);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Add a new key binding
|
||||
// - If the key chord is already in use, the conflicting command is overwritten.
|
||||
// Arguments:
|
||||
// - keys: the key chord that is being bound
|
||||
// - action: the action that the keys are being bound to
|
||||
// Return Value:
|
||||
// - <none>
|
||||
void ActionMap::RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action)
|
||||
{
|
||||
auto cmd{ make_self<Command>() };
|
||||
cmd->RegisterKey(keys);
|
||||
cmd->ActionAndArgs(action);
|
||||
AddAction(*cmd);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
ActionMap();
|
||||
|
||||
// views
|
||||
Windows::Foundation::Collections::IMapView<hstring, Model::ActionAndArgs> AvailableActions();
|
||||
Windows::Foundation::Collections::IMapView<hstring, Model::Command> NameMap();
|
||||
Windows::Foundation::Collections::IMapView<Control::KeyChord, Model::Command> GlobalHotkeys();
|
||||
Windows::Foundation::Collections::IMapView<Control::KeyChord, Model::Command> KeyBindings();
|
||||
|
@ -74,6 +75,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
// modification
|
||||
bool RebindKeys(Control::KeyChord const& oldKeys, Control::KeyChord const& newKeys);
|
||||
void DeleteKeyBinding(Control::KeyChord const& keys);
|
||||
void RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action);
|
||||
|
||||
static Windows::System::VirtualKeyModifiers ConvertVKModifiers(Control::KeyModifiers modifiers);
|
||||
|
||||
|
@ -81,6 +83,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
std::optional<Model::Command> _GetActionByID(const InternalActionID actionID) const;
|
||||
std::optional<Model::Command> _GetActionByKeyChordInternal(Control::KeyChord const& keys) const;
|
||||
|
||||
void _PopulateAvailableActionsWithStandardCommands(std::unordered_map<hstring, Model::ActionAndArgs>& availableActions, std::unordered_set<InternalActionID>& visitedActionIDs) const;
|
||||
void _PopulateNameMapWithSpecialCommands(std::unordered_map<hstring, Model::Command>& nameMap) const;
|
||||
void _PopulateNameMapWithStandardCommands(std::unordered_map<hstring, Model::Command>& nameMap) const;
|
||||
void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map<Control::KeyChord, Model::Command, KeyChordHash, KeyChordEquality>& keyBindingsMap, std::unordered_set<Control::KeyChord, KeyChordHash, KeyChordEquality>& unboundKeys) const;
|
||||
|
@ -90,6 +93,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
void _TryUpdateName(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd);
|
||||
void _TryUpdateKeyChord(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd);
|
||||
|
||||
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionsCache{ nullptr };
|
||||
Windows::Foundation::Collections::IMap<hstring, Model::Command> _NameMapCache{ nullptr };
|
||||
Windows::Foundation::Collections::IMap<Control::KeyChord, Model::Command> _GlobalHotkeysCache{ nullptr };
|
||||
Windows::Foundation::Collections::IMap<Control::KeyChord, Model::Command> _KeyBindingMapCache{ nullptr };
|
||||
|
|
|
@ -13,6 +13,8 @@ namespace Microsoft.Terminal.Settings.Model
|
|||
Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action);
|
||||
[method_name("GetKeyBindingForActionWithArgs")] Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action, IActionArgs actionArgs);
|
||||
|
||||
Windows.Foundation.Collections.IMapView<String, ActionAndArgs> AvailableActions { get; };
|
||||
|
||||
Windows.Foundation.Collections.IMapView<String, Command> NameMap { get; };
|
||||
Windows.Foundation.Collections.IMapView<Microsoft.Terminal.Control.KeyChord, Command> KeyBindings { get; };
|
||||
Windows.Foundation.Collections.IMapView<Microsoft.Terminal.Control.KeyChord, Command> GlobalHotkeys { get; };
|
||||
|
@ -20,7 +22,9 @@ namespace Microsoft.Terminal.Settings.Model
|
|||
|
||||
[default_interface] runtimeclass ActionMap : IActionMapView
|
||||
{
|
||||
Boolean RebindKeys(Microsoft.Terminal.Control.KeyChord oldKeys, Microsoft.Terminal.Control.KeyChord newKeys);
|
||||
void RebindKeys(Microsoft.Terminal.Control.KeyChord oldKeys, Microsoft.Terminal.Control.KeyChord newKeys);
|
||||
void DeleteKeyBinding(Microsoft.Terminal.Control.KeyChord keys);
|
||||
|
||||
void RegisterKeyBinding(Microsoft.Terminal.Control.KeyChord keys, ActionAndArgs action);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(CloseOtherTabs) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(CloseTabsAfter) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(CloseTab) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(CopyText) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(ExecuteCommandline) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(FindMatch) \
|
||||
|
|
173
src/cascadia/TerminalSettingsModel/ApplicationState.cpp
Normal file
173
src/cascadia/TerminalSettingsModel/ApplicationState.cpp
Normal file
|
@ -0,0 +1,173 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "pch.h"
|
||||
#include "ApplicationState.h"
|
||||
#include "CascadiaSettings.h"
|
||||
#include "ApplicationState.g.cpp"
|
||||
|
||||
#include "JsonUtils.h"
|
||||
#include "FileUtils.h"
|
||||
|
||||
constexpr std::wstring_view stateFileName{ L"state.json" };
|
||||
|
||||
namespace Microsoft::Terminal::Settings::Model::JsonUtils
|
||||
{
|
||||
// This trait exists in order to serialize the std::unordered_set for GeneratedProfiles.
|
||||
template<typename T>
|
||||
struct ConversionTrait<std::unordered_set<T>>
|
||||
{
|
||||
std::unordered_set<T> FromJson(const Json::Value& json) const
|
||||
{
|
||||
ConversionTrait<T> trait;
|
||||
std::unordered_set<T> val;
|
||||
val.reserve(json.size());
|
||||
|
||||
for (const auto& element : json)
|
||||
{
|
||||
val.emplace(trait.FromJson(element));
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json) const
|
||||
{
|
||||
ConversionTrait<T> trait;
|
||||
return json.isArray() && std::all_of(json.begin(), json.end(), [trait](const auto& json) -> bool { return trait.CanConvert(json); });
|
||||
}
|
||||
|
||||
Json::Value ToJson(const std::unordered_set<T>& val)
|
||||
{
|
||||
ConversionTrait<T> trait;
|
||||
Json::Value json{ Json::arrayValue };
|
||||
|
||||
for (const auto& key : val)
|
||||
{
|
||||
json.append(trait.ToJson(key));
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
std::string TypeDescription() const
|
||||
{
|
||||
return fmt::format("{}[]", ConversionTrait<GUID>{}.TypeDescription());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
using namespace ::Microsoft::Terminal::Settings::Model;
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
{
|
||||
// Returns the application-global ApplicationState object.
|
||||
Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance()
|
||||
{
|
||||
static auto state = winrt::make_self<ApplicationState>(GetBaseSettingsPath() / stateFileName);
|
||||
return *state;
|
||||
}
|
||||
|
||||
ApplicationState::ApplicationState(std::filesystem::path path) noexcept :
|
||||
_path{ std::move(path) },
|
||||
_throttler{ std::chrono::seconds(1), [this]() { _write(); } }
|
||||
{
|
||||
_read();
|
||||
}
|
||||
|
||||
// The destructor ensures that the last write is flushed to disk before returning.
|
||||
ApplicationState::~ApplicationState()
|
||||
{
|
||||
// This will ensure that we not just cancel the last outstanding timer,
|
||||
// but instead force it to run as soon as possible and wait for it to complete.
|
||||
_throttler.flush();
|
||||
}
|
||||
|
||||
// Re-read the state.json from disk.
|
||||
void ApplicationState::Reload() const noexcept
|
||||
{
|
||||
_read();
|
||||
}
|
||||
|
||||
// Returns the state.json path on the disk.
|
||||
winrt::hstring ApplicationState::FilePath() const noexcept
|
||||
{
|
||||
return winrt::hstring{ _path.wstring() };
|
||||
}
|
||||
|
||||
// Generate all getter/setters
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
|
||||
type ApplicationState::name() const noexcept \
|
||||
{ \
|
||||
const auto state = _state.lock_shared(); \
|
||||
const auto& value = state->name; \
|
||||
return value ? *value : type{ __VA_ARGS__ }; \
|
||||
} \
|
||||
\
|
||||
void ApplicationState::name(const type& value) noexcept \
|
||||
{ \
|
||||
{ \
|
||||
auto state = _state.lock(); \
|
||||
state->name.emplace(value); \
|
||||
} \
|
||||
\
|
||||
_throttler(); \
|
||||
}
|
||||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
|
||||
#undef MTSM_APPLICATION_STATE_GEN
|
||||
|
||||
// Deserializes the state.json at _path into this ApplicationState.
|
||||
// * ANY errors during app state will result in the creation of a new empty state.
|
||||
// * ANY errors during runtime will result in changes being partially ignored.
|
||||
void ApplicationState::_read() const noexcept
|
||||
try
|
||||
{
|
||||
const auto data = ReadUTF8FileIfExists(_path).value_or(std::string{});
|
||||
if (data.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::string errs;
|
||||
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
|
||||
|
||||
Json::Value root;
|
||||
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs))
|
||||
{
|
||||
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
|
||||
}
|
||||
|
||||
auto state = _state.lock();
|
||||
// GetValueForKey() comes in two variants:
|
||||
// * take a std::optional<T> reference
|
||||
// * return std::optional<T> by value
|
||||
// At the time of writing the former version skips missing fields in the json,
|
||||
// but we want to explicitly clear state fields that were removed from state.json.
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey<std::optional<type>>(root, key);
|
||||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
|
||||
#undef MTSM_APPLICATION_STATE_GEN
|
||||
}
|
||||
CATCH_LOG()
|
||||
|
||||
// Serialized this ApplicationState (in `context`) into the state.json at _path.
|
||||
// * Errors are only logged.
|
||||
// * _state->_writeScheduled is set to false, signaling our
|
||||
// setters that _synchronize() needs to be called again.
|
||||
void ApplicationState::_write() const noexcept
|
||||
try
|
||||
{
|
||||
Json::Value root{ Json::objectValue };
|
||||
|
||||
{
|
||||
auto state = _state.lock_shared();
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name);
|
||||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
|
||||
#undef MTSM_APPLICATION_STATE_GEN
|
||||
}
|
||||
|
||||
Json::StreamWriterBuilder wbuilder;
|
||||
const auto content = Json::writeString(wbuilder, root);
|
||||
WriteUTF8FileAtomic(_path, content);
|
||||
}
|
||||
CATCH_LOG()
|
||||
}
|
69
src/cascadia/TerminalSettingsModel/ApplicationState.h
Normal file
69
src/cascadia/TerminalSettingsModel/ApplicationState.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- ApplicationState.h
|
||||
|
||||
Abstract:
|
||||
- If the CascadiaSettings class were AppData, then this class would be LocalAppData.
|
||||
Put anything in here that you wouldn't want to be stored next to user-editable settings.
|
||||
- Modify ApplicationState.idl and MTSM_APPLICATION_STATE_FIELDS to add new fields.
|
||||
--*/
|
||||
#pragma once
|
||||
|
||||
#include "ApplicationState.g.h"
|
||||
|
||||
#include <inc/cppwinrt_utils.h>
|
||||
#include <til/mutex.h>
|
||||
#include <til/throttled_func.h>
|
||||
|
||||
// This macro generates all getters and setters for ApplicationState.
|
||||
// It provides X with the following arguments:
|
||||
// (type, function name, JSON key, ...variadic construction arguments)
|
||||
#define MTSM_APPLICATION_STATE_FIELDS(X) \
|
||||
X(std::unordered_set<winrt::guid>, GeneratedProfiles, "generatedProfiles")
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
{
|
||||
struct ApplicationState : ApplicationStateT<ApplicationState>
|
||||
{
|
||||
static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance();
|
||||
|
||||
ApplicationState(std::filesystem::path path) noexcept;
|
||||
~ApplicationState();
|
||||
|
||||
// Methods
|
||||
void Reload() const noexcept;
|
||||
|
||||
// General getters/setters
|
||||
winrt::hstring FilePath() const noexcept;
|
||||
|
||||
// State getters/setters
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
|
||||
type name() const noexcept; \
|
||||
void name(const type& value) noexcept;
|
||||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
|
||||
#undef MTSM_APPLICATION_STATE_GEN
|
||||
|
||||
private:
|
||||
struct state_t
|
||||
{
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) std::optional<type> name{ __VA_ARGS__ };
|
||||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
|
||||
#undef MTSM_APPLICATION_STATE_GEN
|
||||
};
|
||||
|
||||
void _write() const noexcept;
|
||||
void _read() const noexcept;
|
||||
|
||||
std::filesystem::path _path;
|
||||
til::shared_mutex<state_t> _state;
|
||||
til::throttled_func_trailing<> _throttler;
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
|
||||
{
|
||||
BASIC_FACTORY(ApplicationState);
|
||||
}
|
13
src/cascadia/TerminalSettingsModel/ApplicationState.idl
Normal file
13
src/cascadia/TerminalSettingsModel/ApplicationState.idl
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft.Terminal.Settings.Model
|
||||
{
|
||||
[default_interface] runtimeclass ApplicationState {
|
||||
static ApplicationState SharedInstance();
|
||||
|
||||
void Reload();
|
||||
|
||||
String FilePath { get; };
|
||||
}
|
||||
}
|
|
@ -296,6 +296,13 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate
|
|||
duplicated->##settingName(source.##settingName()); \
|
||||
}
|
||||
|
||||
#define DUPLICATE_FONT_SETTING_MACRO(settingName) \
|
||||
if (source.FontInfo().Has##settingName() || \
|
||||
(source.FontInfo().##settingName##OverrideSource() != nullptr && source.FontInfo().##settingName##OverrideSource().SourceProfile().Origin() != OriginTag::ProfilesDefaults)) \
|
||||
{ \
|
||||
duplicated->FontInfo().##settingName(source.FontInfo().##settingName()); \
|
||||
}
|
||||
|
||||
#define DUPLICATE_APPEARANCE_SETTING_MACRO(settingName) \
|
||||
if (source.DefaultAppearance().Has##settingName() || \
|
||||
(source.DefaultAppearance().##settingName##OverrideSource() != nullptr && source.DefaultAppearance().##settingName##OverrideSource().SourceProfile().Origin() != OriginTag::ProfilesDefaults)) \
|
||||
|
@ -312,9 +319,6 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate
|
|||
DUPLICATE_SETTING_MACRO(UseAcrylic);
|
||||
DUPLICATE_SETTING_MACRO(AcrylicOpacity);
|
||||
DUPLICATE_SETTING_MACRO(ScrollState);
|
||||
DUPLICATE_SETTING_MACRO(FontFace);
|
||||
DUPLICATE_SETTING_MACRO(FontSize);
|
||||
DUPLICATE_SETTING_MACRO(FontWeight);
|
||||
DUPLICATE_SETTING_MACRO(Padding);
|
||||
DUPLICATE_SETTING_MACRO(Commandline);
|
||||
DUPLICATE_SETTING_MACRO(StartingDirectory);
|
||||
|
@ -326,6 +330,10 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate
|
|||
DUPLICATE_SETTING_MACRO(AltGrAliasing);
|
||||
DUPLICATE_SETTING_MACRO(BellStyle);
|
||||
|
||||
DUPLICATE_FONT_SETTING_MACRO(FontFace);
|
||||
DUPLICATE_FONT_SETTING_MACRO(FontSize);
|
||||
DUPLICATE_FONT_SETTING_MACRO(FontWeight);
|
||||
|
||||
DUPLICATE_APPEARANCE_SETTING_MACRO(ColorSchemeName);
|
||||
DUPLICATE_APPEARANCE_SETTING_MACRO(Foreground);
|
||||
DUPLICATE_APPEARANCE_SETTING_MACRO(Background);
|
||||
|
|
|
@ -147,9 +147,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
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 void _WriteSettings(std::string_view content, const hstring filepath);
|
||||
static const std::filesystem::path& _SettingsPath();
|
||||
static std::optional<std::string> _ReadUserSettings();
|
||||
static std::optional<std::string> _ReadFile(HANDLE hFile);
|
||||
|
||||
std::optional<guid> _GetProfileGuidByName(const hstring) const;
|
||||
std::optional<guid> _GetProfileGuidByIndex(std::optional<int> index) const;
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
#include <fmt/chrono.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
#include <WtExeUtils.h>
|
||||
|
||||
// defaults.h is a file containing the default json settings in a std::string_view
|
||||
#include "defaults.h"
|
||||
#include "defaults-universal.h"
|
||||
|
@ -17,12 +15,15 @@
|
|||
// Both defaults.h and userDefaults.h are generated at build time into the
|
||||
// "Generated Files" directory.
|
||||
|
||||
#include "ApplicationState.h"
|
||||
#include "FileUtils.h"
|
||||
|
||||
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
|
||||
using namespace ::Microsoft::Console;
|
||||
using namespace ::Microsoft::Terminal::Settings::Model;
|
||||
|
||||
static constexpr std::wstring_view SettingsFilename{ L"settings.json" };
|
||||
static constexpr std::wstring_view LegacySettingsFilename{ L"profiles.json" };
|
||||
static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" };
|
||||
|
||||
static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" };
|
||||
|
||||
|
@ -40,7 +41,6 @@ static constexpr std::string_view GuidKey{ "guid" };
|
|||
|
||||
static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" };
|
||||
|
||||
static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
|
||||
static constexpr std::string_view SettingsSchemaFragment{ "\n"
|
||||
R"( "$schema": "https://aka.ms/terminal-profiles-schema")" };
|
||||
|
||||
|
@ -234,7 +234,7 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings::
|
|||
|
||||
try
|
||||
{
|
||||
_WriteSettings(resultPtr->_userSettingsString, CascadiaSettings::SettingsPath());
|
||||
WriteUTF8FileAtomic(_SettingsPath(), resultPtr->_userSettingsString);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
@ -491,23 +491,11 @@ std::unordered_set<std::string> CascadiaSettings::_AccumulateJsonFilesInDirector
|
|||
{
|
||||
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)
|
||||
try
|
||||
{
|
||||
LOG_LAST_ERROR();
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto fileData = _ReadFile(hFile.get()).value();
|
||||
jsonFiles.emplace(fileData);
|
||||
jsonFiles.emplace(ReadUTF8File(fragmentExt.path()));
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
}
|
||||
return jsonFiles;
|
||||
|
@ -637,13 +625,8 @@ void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool is
|
|||
Json::Value CascadiaSettings::_ParseUtf8JsonString(std::string_view fileData)
|
||||
{
|
||||
Json::Value result;
|
||||
// Ignore UTF-8 BOM
|
||||
auto actualDataStart = fileData.data();
|
||||
const auto actualDataStart = fileData.data();
|
||||
const auto actualDataEnd = fileData.data() + fileData.size();
|
||||
if (fileData.compare(0, Utf8Bom.size(), Utf8Bom) == 0)
|
||||
{
|
||||
actualDataStart += Utf8Bom.size();
|
||||
}
|
||||
|
||||
std::string errs; // This string will receive any error text from failing to parse.
|
||||
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
|
||||
|
@ -693,8 +676,7 @@ bool CascadiaSettings::_PrependSchemaDirective()
|
|||
// them into the user's settings at the end of the list of profiles.
|
||||
// - Does not reformat the user's settings file.
|
||||
// - Does not write the file! Only modifies in-place the _userSettingsString
|
||||
// member. Callers should make sure to call
|
||||
// _WriteSettings(_userSettingsString) to make sure to persist these changes!
|
||||
// member. Callers should make sure to persist these changes (see WriteSettingsToDisk).
|
||||
// - Assumes that the `profiles` object is at an indentation of 4 spaces, and
|
||||
// therefore each profile should be indented 8 spaces. If the user's settings
|
||||
// have a different indentation, we'll still insert valid json, it'll just be
|
||||
|
@ -1056,28 +1038,15 @@ winrt::com_ptr<ColorScheme> CascadiaSettings::_FindMatchingColorScheme(const Jso
|
|||
}
|
||||
|
||||
// Method Description:
|
||||
// - Writes the given content in UTF-8 to a settings file using the Win32 APIS's.
|
||||
// Will overwrite any existing content in the file.
|
||||
// - Returns the path of the settings.json file.
|
||||
// Arguments:
|
||||
// - content: the given string of content to write to the file.
|
||||
// Return Value:
|
||||
// - <none>
|
||||
// This can throw an exception if we fail to open the file for writing, or we
|
||||
// fail to write the file
|
||||
void CascadiaSettings::_WriteSettings(const std::string_view content, const hstring filepath)
|
||||
// Return Value:
|
||||
// - Returns a path in 80% of cases. I measured!
|
||||
const std::filesystem::path& CascadiaSettings::_SettingsPath()
|
||||
{
|
||||
wil::unique_hfile hOut{ CreateFileW(filepath.c_str(),
|
||||
GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
if (!hOut)
|
||||
{
|
||||
THROW_LAST_ERROR();
|
||||
}
|
||||
THROW_LAST_ERROR_IF(!WriteFile(hOut.get(), content.data(), gsl::narrow<DWORD>(content.size()), nullptr, nullptr));
|
||||
static const auto path = GetBaseSettingsPath() / SettingsFilename;
|
||||
return path;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -1091,92 +1060,7 @@ void CascadiaSettings::_WriteSettings(const std::string_view content, const hstr
|
|||
// from reading the file
|
||||
std::optional<std::string> CascadiaSettings::_ReadUserSettings()
|
||||
{
|
||||
const auto pathToSettingsFile{ CascadiaSettings::SettingsPath() };
|
||||
wil::unique_hfile hFile{ CreateFileW(pathToSettingsFile.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
|
||||
if (!hFile)
|
||||
{
|
||||
// GH#5186 - We moved from profiles.json to settings.json; we want to
|
||||
// migrate any file we find. We're using MoveFile in case their settings.json
|
||||
// is a symbolic link.
|
||||
std::filesystem::path pathToLegacySettingsFile{ std::wstring_view{ pathToSettingsFile } };
|
||||
pathToLegacySettingsFile.replace_filename(LegacySettingsFilename);
|
||||
|
||||
wil::unique_hfile hLegacyFile{ CreateFileW(pathToLegacySettingsFile.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
|
||||
if (hLegacyFile)
|
||||
{
|
||||
// Close the file handle, move it, and re-open the file in its new location.
|
||||
hLegacyFile.reset();
|
||||
|
||||
// Note: We're unsure if this is unsafe. Theoretically it's possible
|
||||
// that two instances of the app will try and move the settings file
|
||||
// simultaneously. We don't know what might happen in that scenario,
|
||||
// but we're also not sure how to safely lock the file to prevent
|
||||
// that from occurring.
|
||||
THROW_LAST_ERROR_IF(!MoveFile(pathToLegacySettingsFile.c_str(),
|
||||
pathToSettingsFile.c_str()));
|
||||
|
||||
hFile.reset(CreateFileW(pathToSettingsFile.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr));
|
||||
|
||||
// hFile shouldn't be INVALID. That's unexpected - We just moved the
|
||||
// file, we should be able to open it. Throw the error so we can get
|
||||
// some information here.
|
||||
THROW_LAST_ERROR_IF(!hFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the roaming file didn't exist, and the local file doesn't exist,
|
||||
// that's fine. Just log the error and return nullopt - we'll
|
||||
// create the defaults.
|
||||
LOG_LAST_ERROR();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return _ReadFile(hFile.get());
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Reads the content in UTF-8 encoding of the given file using the Win32 APIs
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - an optional with the content of the file if we were able to read it. If we
|
||||
// fail to read it, this can throw an exception from reading the file
|
||||
std::optional<std::string> CascadiaSettings::_ReadFile(HANDLE hFile)
|
||||
{
|
||||
// fileSize is in bytes
|
||||
const auto fileSize = GetFileSize(hFile, nullptr);
|
||||
THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE);
|
||||
|
||||
auto utf8buffer = std::make_unique<char[]>(fileSize);
|
||||
|
||||
DWORD bytesRead = 0;
|
||||
THROW_LAST_ERROR_IF(!ReadFile(hFile, utf8buffer.get(), fileSize, &bytesRead, nullptr));
|
||||
|
||||
// convert buffer to UTF-8 string
|
||||
std::string utf8string(utf8buffer.get(), fileSize);
|
||||
|
||||
return { utf8string };
|
||||
return ReadUTF8FileIfExists(_SettingsPath());
|
||||
}
|
||||
|
||||
// function Description:
|
||||
|
@ -1191,23 +1075,7 @@ std::optional<std::string> CascadiaSettings::_ReadFile(HANDLE hFile)
|
|||
// - the full path to the settings file
|
||||
winrt::hstring CascadiaSettings::SettingsPath()
|
||||
{
|
||||
wil::unique_cotaskmem_string localAppDataFolder;
|
||||
// KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return
|
||||
// the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests.
|
||||
// Using this flag allows us to avoid Windows.Storage.ApplicationData completely.
|
||||
THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_FORCE_APP_DATA_REDIRECTION, nullptr, &localAppDataFolder));
|
||||
|
||||
std::filesystem::path parentDirectoryForSettingsFile{ localAppDataFolder.get() };
|
||||
|
||||
if (!IsPackaged())
|
||||
{
|
||||
parentDirectoryForSettingsFile /= UnpackagedSettingsFolderName;
|
||||
}
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
std::filesystem::create_directories(parentDirectoryForSettingsFile);
|
||||
|
||||
return winrt::hstring{ (parentDirectoryForSettingsFile / SettingsFilename).wstring() };
|
||||
return winrt::hstring{ _SettingsPath().wstring() };
|
||||
}
|
||||
|
||||
winrt::hstring CascadiaSettings::DefaultSettingsPath()
|
||||
|
@ -1222,15 +1090,12 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath()
|
|||
// directory as the exe, that will work for unpackaged scenarios as well. So
|
||||
// let's try that.
|
||||
|
||||
HMODULE hModule = GetModuleHandle(nullptr);
|
||||
THROW_LAST_ERROR_IF(hModule == nullptr);
|
||||
|
||||
std::wstring exePathString;
|
||||
THROW_IF_FAILED(wil::GetModuleFileNameW(hModule, exePathString));
|
||||
THROW_IF_FAILED(wil::GetModuleFileNameW(nullptr, exePathString));
|
||||
|
||||
const std::filesystem::path exePath{ exePathString };
|
||||
const std::filesystem::path rootDir = exePath.parent_path();
|
||||
return winrt::hstring{ (rootDir / DefaultsFilename).wstring() };
|
||||
std::filesystem::path path{ exePathString };
|
||||
path.replace_filename(DefaultsFilename);
|
||||
return winrt::hstring{ path.wstring() };
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
|
@ -1275,15 +1140,13 @@ const Json::Value& CascadiaSettings::_GetDisabledProfileSourcesJsonObject(const
|
|||
// - <none>
|
||||
void CascadiaSettings::WriteSettingsToDisk() const
|
||||
{
|
||||
const auto settingsPath{ CascadiaSettings::SettingsPath() };
|
||||
const auto settingsPath = _SettingsPath();
|
||||
|
||||
try
|
||||
{
|
||||
// create a timestamped backup file
|
||||
const auto clock{ std::chrono::system_clock() };
|
||||
const auto timeStamp{ clock.to_time_t(clock.now()) };
|
||||
const winrt::hstring backupSettingsPath{ fmt::format(L"{}.{:%Y-%m-%dT%H-%M-%S}.backup", settingsPath, fmt::localtime(timeStamp)) };
|
||||
_WriteSettings(_userSettingsString, backupSettingsPath);
|
||||
const auto backupSettingsPath = fmt::format(L"{}.{:%Y-%m-%dT%H-%M-%S}.backup", settingsPath.wstring(), fmt::localtime(std::time(nullptr)));
|
||||
WriteUTF8File(backupSettingsPath, _userSettingsString);
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
|
@ -1293,7 +1156,7 @@ void CascadiaSettings::WriteSettingsToDisk() const
|
|||
wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
|
||||
|
||||
const auto styledString{ Json::writeString(wbuilder, ToJson()) };
|
||||
_WriteSettings(styledString, settingsPath);
|
||||
WriteUTF8FileAtomic(settingsPath, styledString);
|
||||
|
||||
// Persists the default terminal choice
|
||||
//
|
||||
|
|
137
src/cascadia/TerminalSettingsModel/FileUtils.cpp
Normal file
137
src/cascadia/TerminalSettingsModel/FileUtils.cpp
Normal file
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "pch.h"
|
||||
#include "FileUtils.h"
|
||||
|
||||
#include <appmodel.h>
|
||||
#include <shlobj.h>
|
||||
#include <WtExeUtils.h>
|
||||
|
||||
static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
|
||||
static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" };
|
||||
|
||||
namespace Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
// Returns a path like C:\Users\<username>\AppData\Local\Packages\<packagename>\LocalState
|
||||
// You can put your settings.json or state.json in this directory.
|
||||
std::filesystem::path GetBaseSettingsPath()
|
||||
{
|
||||
static std::filesystem::path baseSettingsPath = []() {
|
||||
wil::unique_cotaskmem_string localAppDataFolder;
|
||||
// KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return
|
||||
// the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests.
|
||||
// Using this flag allows us to avoid Windows.Storage.ApplicationData completely.
|
||||
THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_FORCE_APP_DATA_REDIRECTION, nullptr, &localAppDataFolder));
|
||||
|
||||
std::filesystem::path parentDirectoryForSettingsFile{ localAppDataFolder.get() };
|
||||
|
||||
if (!IsPackaged())
|
||||
{
|
||||
parentDirectoryForSettingsFile /= UnpackagedSettingsFolderName;
|
||||
}
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
std::filesystem::create_directories(parentDirectoryForSettingsFile);
|
||||
|
||||
return parentDirectoryForSettingsFile;
|
||||
}();
|
||||
return baseSettingsPath;
|
||||
}
|
||||
|
||||
// Tries to read a file somewhat atomically without locking it.
|
||||
// Strips the UTF8 BOM if it exists.
|
||||
std::string ReadUTF8File(const std::filesystem::path& path)
|
||||
{
|
||||
// From some casual observations we can determine that:
|
||||
// * ReadFile() always returns the requested amount of data (unless the file is smaller)
|
||||
// * It's unlikely that the file was changed between GetFileSize() and ReadFile()
|
||||
// -> Lets add a retry-loop just in case, to not fail if the file size changed while reading.
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!file);
|
||||
|
||||
const auto fileSize = GetFileSize(file.get(), nullptr);
|
||||
THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE);
|
||||
|
||||
// By making our buffer just slightly larger we can detect if
|
||||
// the file size changed and we've failed to read the full file.
|
||||
std::string buffer(static_cast<size_t>(fileSize) + 1, '\0');
|
||||
DWORD bytesRead = 0;
|
||||
THROW_IF_WIN32_BOOL_FALSE(ReadFile(file.get(), buffer.data(), gsl::narrow<DWORD>(buffer.size()), &bytesRead, nullptr));
|
||||
|
||||
// This implementation isn't atomic as we'd need to use an exclusive file lock.
|
||||
// But this would be annoying for users as it forces them to close the file in their editor.
|
||||
// The next best alternative is to at least try to detect file changes and retry the read.
|
||||
if (bytesRead != fileSize)
|
||||
{
|
||||
// This continue is unlikely to be hit (see the prior for loop comment).
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
// As mentioned before our buffer was allocated oversized.
|
||||
buffer.resize(bytesRead);
|
||||
|
||||
if (til::starts_with(buffer, Utf8Bom))
|
||||
{
|
||||
// Yeah this memmove()s the entire content.
|
||||
// But I don't really want to deal with UTF8 BOMs any more than necessary,
|
||||
// as basically not a single editor writes a BOM for UTF8.
|
||||
buffer.erase(0, Utf8Bom.size());
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
THROW_WIN32_MSG(ERROR_READ_FAULT, "file size changed while reading");
|
||||
}
|
||||
|
||||
// Same as ReadUTF8File, but returns an empty optional, if the file couldn't be opened.
|
||||
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return { ReadUTF8File(path) };
|
||||
}
|
||||
catch (const wil::ResultException& exception)
|
||||
{
|
||||
if (exception.GetErrorCode() == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteUTF8File(const std::filesystem::path& path, const std::string_view content)
|
||||
{
|
||||
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!file);
|
||||
|
||||
const auto fileSize = gsl::narrow<DWORD>(content.size());
|
||||
DWORD bytesWritten = 0;
|
||||
THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr));
|
||||
|
||||
if (bytesWritten != fileSize)
|
||||
{
|
||||
THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file");
|
||||
}
|
||||
}
|
||||
|
||||
void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content)
|
||||
{
|
||||
auto tmpPath = path;
|
||||
tmpPath += L".tmp";
|
||||
|
||||
// Writing to a file isn't atomic, but...
|
||||
WriteUTF8File(tmpPath, content);
|
||||
|
||||
// renaming one is (supposed to be) atomic.
|
||||
// Wait... "supposed to be"!? Well it's technically not always atomic,
|
||||
// but it's pretty darn close to it, so... better than nothing.
|
||||
std::filesystem::rename(tmpPath, path);
|
||||
}
|
||||
}
|
11
src/cascadia/TerminalSettingsModel/FileUtils.h
Normal file
11
src/cascadia/TerminalSettingsModel/FileUtils.h
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
std::filesystem::path GetBaseSettingsPath();
|
||||
std::string ReadUTF8File(const std::filesystem::path& path);
|
||||
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path);
|
||||
void WriteUTF8File(const std::filesystem::path& path, const std::string_view content);
|
||||
void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content);
|
||||
}
|
86
src/cascadia/TerminalSettingsModel/FontConfig.cpp
Normal file
86
src/cascadia/TerminalSettingsModel/FontConfig.cpp
Normal file
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "pch.h"
|
||||
#include "FontConfig.h"
|
||||
#include "FontConfig.g.cpp"
|
||||
#include "TerminalSettingsSerializationHelpers.h"
|
||||
#include "JsonUtils.h"
|
||||
|
||||
using namespace Microsoft::Terminal::Settings::Model;
|
||||
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
|
||||
|
||||
static constexpr std::string_view FontInfoKey{ "font" };
|
||||
static constexpr std::string_view FontFaceKey{ "face" };
|
||||
static constexpr std::string_view FontSizeKey{ "size" };
|
||||
static constexpr std::string_view FontWeightKey{ "weight" };
|
||||
static constexpr std::string_view LegacyFontFaceKey{ "fontFace" };
|
||||
static constexpr std::string_view LegacyFontSizeKey{ "fontSize" };
|
||||
static constexpr std::string_view LegacyFontWeightKey{ "fontWeight" };
|
||||
|
||||
winrt::Microsoft::Terminal::Settings::Model::implementation::FontConfig::FontConfig(winrt::weak_ref<Profile> sourceProfile) :
|
||||
_sourceProfile(std::move(sourceProfile))
|
||||
{
|
||||
}
|
||||
|
||||
winrt::com_ptr<FontConfig> FontConfig::CopyFontInfo(const winrt::com_ptr<FontConfig> source, winrt::weak_ref<Profile> sourceProfile)
|
||||
{
|
||||
auto fontInfo{ winrt::make_self<FontConfig>(std::move(sourceProfile)) };
|
||||
fontInfo->_FontFace = source->_FontFace;
|
||||
fontInfo->_FontSize = source->_FontSize;
|
||||
fontInfo->_FontWeight = source->_FontWeight;
|
||||
return fontInfo;
|
||||
}
|
||||
|
||||
Json::Value FontConfig::ToJson() const
|
||||
{
|
||||
Json::Value json{ Json::ValueType::objectValue };
|
||||
|
||||
JsonUtils::SetValueForKey(json, FontFaceKey, _FontFace);
|
||||
JsonUtils::SetValueForKey(json, FontSizeKey, _FontSize);
|
||||
JsonUtils::SetValueForKey(json, FontWeightKey, _FontWeight);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Layer values from the given json object on top of the existing properties
|
||||
// of this object. For any keys we're expecting to be able to parse in the
|
||||
// given object, we'll parse them and replace our settings with values from
|
||||
// the new json object. Properties that _aren't_ in the json object will _not_
|
||||
// be replaced.
|
||||
// - Optional values that are set to `null` in the json object
|
||||
// will be set to nullopt.
|
||||
// - This is similar to Profile::LayerJson but for FontConfig
|
||||
// Arguments:
|
||||
// - json: an object which should be a partial serialization of a FontConfig object.
|
||||
void FontConfig::LayerJson(const Json::Value& json)
|
||||
{
|
||||
// Legacy users may not have a font object defined in their profile,
|
||||
// so check for that before we decide how to parse this
|
||||
if (json.isMember(JsonKey(FontInfoKey)))
|
||||
{
|
||||
// A font object is defined, use that
|
||||
const auto fontInfoJson = json[JsonKey(FontInfoKey)];
|
||||
JsonUtils::GetValueForKey(fontInfoJson, FontFaceKey, _FontFace);
|
||||
JsonUtils::GetValueForKey(fontInfoJson, FontSizeKey, _FontSize);
|
||||
JsonUtils::GetValueForKey(fontInfoJson, FontWeightKey, _FontWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No font object is defined
|
||||
JsonUtils::GetValueForKey(json, LegacyFontFaceKey, _FontFace);
|
||||
JsonUtils::GetValueForKey(json, LegacyFontSizeKey, _FontSize);
|
||||
JsonUtils::GetValueForKey(json, LegacyFontWeightKey, _FontWeight);
|
||||
}
|
||||
}
|
||||
|
||||
bool FontConfig::HasAnyOptionSet() const
|
||||
{
|
||||
return HasFontFace() || HasFontSize() || HasFontWeight();
|
||||
}
|
||||
|
||||
winrt::Microsoft::Terminal::Settings::Model::Profile FontConfig::SourceProfile()
|
||||
{
|
||||
return _sourceProfile.get();
|
||||
}
|
46
src/cascadia/TerminalSettingsModel/FontConfig.h
Normal file
46
src/cascadia/TerminalSettingsModel/FontConfig.h
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- FontConfig
|
||||
|
||||
Abstract:
|
||||
- The implementation of the FontConfig winrt class. Provides settings related
|
||||
to the font settings of the terminal, for the terminal control.
|
||||
|
||||
Author(s):
|
||||
- Pankaj Bhojwani - June 2021
|
||||
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "pch.h"
|
||||
#include "FontConfig.g.h"
|
||||
#include "JsonUtils.h"
|
||||
#include "../inc/cppwinrt_utils.h"
|
||||
#include "IInheritable.h"
|
||||
#include <DefaultSettings.h>
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
{
|
||||
struct FontConfig : FontConfigT<FontConfig>, IInheritable<FontConfig>
|
||||
{
|
||||
public:
|
||||
FontConfig(winrt::weak_ref<Profile> sourceProfile);
|
||||
static winrt::com_ptr<FontConfig> CopyFontInfo(const winrt::com_ptr<FontConfig> source, winrt::weak_ref<Profile> sourceProfile);
|
||||
Json::Value ToJson() const;
|
||||
void LayerJson(const Json::Value& json);
|
||||
bool HasAnyOptionSet() const;
|
||||
|
||||
Model::Profile SourceProfile();
|
||||
|
||||
INHERITABLE_SETTING(Model::FontConfig, hstring, FontFace, DEFAULT_FONT_FACE);
|
||||
INHERITABLE_SETTING(Model::FontConfig, int32_t, FontSize, DEFAULT_FONT_SIZE);
|
||||
INHERITABLE_SETTING(Model::FontConfig, Windows::UI::Text::FontWeight, FontWeight, DEFAULT_FONT_WEIGHT);
|
||||
|
||||
private:
|
||||
winrt::weak_ref<Profile> _sourceProfile;
|
||||
};
|
||||
}
|
20
src/cascadia/TerminalSettingsModel/FontConfig.idl
Normal file
20
src/cascadia/TerminalSettingsModel/FontConfig.idl
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import "Profile.idl";
|
||||
#include "IInheritable.idl.h"
|
||||
|
||||
#define INHERITABLE_FONT_SETTING(Type, Name) \
|
||||
_BASE_INHERITABLE_SETTING(Type, Name); \
|
||||
Microsoft.Terminal.Settings.Model.FontConfig Name##OverrideSource { get; }
|
||||
|
||||
namespace Microsoft.Terminal.Settings.Model
|
||||
{
|
||||
[default_interface] runtimeclass FontConfig {
|
||||
Microsoft.Terminal.Settings.Model.Profile SourceProfile { get; };
|
||||
|
||||
INHERITABLE_FONT_SETTING(String, FontFace);
|
||||
INHERITABLE_FONT_SETTING(Int32, FontSize);
|
||||
INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight);
|
||||
}
|
||||
}
|
|
@ -32,6 +32,9 @@
|
|||
<DependentUpon>ActionMap.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="AzureCloudShellGenerator.h" />
|
||||
<ClInclude Include="ApplicationState.h">
|
||||
<DependentUpon>ApplicationState.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="CascadiaSettings.h">
|
||||
<DependentUpon>CascadiaSettings.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
|
@ -42,6 +45,7 @@
|
|||
<DependentUpon>Command.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="DefaultProfileUtils.h" />
|
||||
<ClInclude Include="FileUtils.h" />
|
||||
<ClInclude Include="GlobalAppSettings.h">
|
||||
<DependentUpon>GlobalAppSettings.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
|
@ -59,6 +63,9 @@
|
|||
<ClInclude Include="AppearanceConfig.h">
|
||||
<DependentUpon>AppearanceConfig.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FontConfig.h">
|
||||
<DependentUpon>FontConfig.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="EnumMappings.h">
|
||||
<DependentUpon>EnumMappings.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
|
@ -97,6 +104,9 @@
|
|||
<DependentUpon>ActionMap.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="AzureCloudShellGenerator.cpp" />
|
||||
<ClCompile Include="ApplicationState.cpp">
|
||||
<DependentUpon>ApplicationState.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="CascadiaSettings.cpp">
|
||||
<DependentUpon>CascadiaSettings.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
|
@ -110,6 +120,7 @@
|
|||
<DependentUpon>Command.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="DefaultProfileUtils.cpp" />
|
||||
<ClCompile Include="FileUtils.cpp" />
|
||||
<ClCompile Include="GlobalAppSettings.cpp">
|
||||
<DependentUpon>GlobalAppSettings.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
|
@ -123,6 +134,9 @@
|
|||
<ClCompile Include="AppearanceConfig.cpp">
|
||||
<DependentUpon>AppearanceConfig.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="FontConfig.cpp">
|
||||
<DependentUpon>FontConfig.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="TerminalSettings.cpp">
|
||||
<DependentUpon>TerminalSettings.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
|
@ -141,6 +155,7 @@
|
|||
<ItemGroup>
|
||||
<Midl Include="ActionArgs.idl" />
|
||||
<Midl Include="ActionMap.idl" />
|
||||
<Midl Include="ApplicationState.idl" />
|
||||
<Midl Include="CascadiaSettings.idl" />
|
||||
<Midl Include="ColorScheme.idl" />
|
||||
<Midl Include="Command.idl" />
|
||||
|
@ -154,6 +169,7 @@
|
|||
<Midl Include="KeyChordSerialization.idl" />
|
||||
<Midl Include="AppearanceConfig.idl" />
|
||||
<Midl Include="IAppearanceConfig.idl" />
|
||||
<Midl Include="FontConfig.idl" />
|
||||
</ItemGroup>
|
||||
<!-- ========================= Misc Files ======================== -->
|
||||
<ItemGroup>
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
<Midl Include="TerminalSettings.idl" />
|
||||
<Midl Include="AppearanceConfig.idl" />
|
||||
<Midl Include="IAppearanceConfig.idl" />
|
||||
<Midl Include="FontConfig.idl" />
|
||||
<Midl Include="DefaultTerminal.idl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "LegacyProfileGeneratorNamespaces.h"
|
||||
#include "TerminalSettingsSerializationHelpers.h"
|
||||
#include "AppearanceConfig.h"
|
||||
#include "FontConfig.h"
|
||||
|
||||
#include "Profile.g.cpp"
|
||||
|
||||
|
@ -34,9 +35,7 @@ static constexpr std::string_view AltGrAliasingKey{ "altGrAliasing" };
|
|||
|
||||
static constexpr std::string_view ConnectionTypeKey{ "connectionType" };
|
||||
static constexpr std::string_view CommandlineKey{ "commandline" };
|
||||
static constexpr std::string_view FontFaceKey{ "fontFace" };
|
||||
static constexpr std::string_view FontSizeKey{ "fontSize" };
|
||||
static constexpr std::string_view FontWeightKey{ "fontWeight" };
|
||||
static constexpr std::string_view FontInfoKey{ "font" };
|
||||
static constexpr std::string_view AcrylicTransparencyKey{ "acrylicOpacity" };
|
||||
static constexpr std::string_view UseAcrylicKey{ "useAcrylic" };
|
||||
static constexpr std::string_view ScrollbarStateKey{ "scrollbarState" };
|
||||
|
@ -76,9 +75,6 @@ winrt::com_ptr<Profile> Profile::CopySettings(winrt::com_ptr<Profile> source)
|
|||
profile->_UseAcrylic = source->_UseAcrylic;
|
||||
profile->_AcrylicOpacity = source->_AcrylicOpacity;
|
||||
profile->_ScrollState = source->_ScrollState;
|
||||
profile->_FontFace = source->_FontFace;
|
||||
profile->_FontSize = source->_FontSize;
|
||||
profile->_FontWeight = source->_FontWeight;
|
||||
profile->_Padding = source->_Padding;
|
||||
profile->_Commandline = source->_Commandline;
|
||||
profile->_StartingDirectory = source->_StartingDirectory;
|
||||
|
@ -92,8 +88,14 @@ winrt::com_ptr<Profile> Profile::CopySettings(winrt::com_ptr<Profile> source)
|
|||
profile->_ConnectionType = source->_ConnectionType;
|
||||
profile->_Origin = source->_Origin;
|
||||
|
||||
// Copy over the appearance
|
||||
// Copy over the font info
|
||||
const auto weakRefToProfile = weak_ref<Model::Profile>(*profile);
|
||||
winrt::com_ptr<FontConfig> sourceFontInfoImpl;
|
||||
sourceFontInfoImpl.copy_from(winrt::get_self<FontConfig>(source->_FontInfo));
|
||||
auto copiedFontInfo = FontConfig::CopyFontInfo(sourceFontInfoImpl, weakRefToProfile);
|
||||
profile->_FontInfo = *copiedFontInfo;
|
||||
|
||||
// Copy over the appearance
|
||||
winrt::com_ptr<AppearanceConfig> sourceDefaultAppearanceImpl;
|
||||
sourceDefaultAppearanceImpl.copy_from(winrt::get_self<AppearanceConfig>(source->_DefaultAppearance));
|
||||
auto copiedDefaultAppearance = AppearanceConfig::CopyAppearance(sourceDefaultAppearanceImpl, weakRefToProfile);
|
||||
|
@ -315,6 +317,10 @@ void Profile::LayerJson(const Json::Value& json)
|
|||
auto defaultAppearanceImpl = winrt::get_self<implementation::AppearanceConfig>(_DefaultAppearance);
|
||||
defaultAppearanceImpl->LayerJson(json);
|
||||
|
||||
// Font Settings
|
||||
auto fontInfoImpl = winrt::get_self<implementation::FontConfig>(_FontInfo);
|
||||
fontInfoImpl->LayerJson(json);
|
||||
|
||||
// Profile-specific Settings
|
||||
JsonUtils::GetValueForKey(json, NameKey, _Name);
|
||||
JsonUtils::GetValueForKey(json, GuidKey, _Guid);
|
||||
|
@ -328,11 +334,8 @@ void Profile::LayerJson(const Json::Value& json)
|
|||
JsonUtils::GetValueForKey(json, TabTitleKey, _TabTitle);
|
||||
|
||||
// Control Settings
|
||||
JsonUtils::GetValueForKey(json, FontWeightKey, _FontWeight);
|
||||
JsonUtils::GetValueForKey(json, ConnectionTypeKey, _ConnectionType);
|
||||
JsonUtils::GetValueForKey(json, CommandlineKey, _Commandline);
|
||||
JsonUtils::GetValueForKey(json, FontFaceKey, _FontFace);
|
||||
JsonUtils::GetValueForKey(json, FontSizeKey, _FontSize);
|
||||
JsonUtils::GetValueForKey(json, AcrylicTransparencyKey, _AcrylicOpacity);
|
||||
JsonUtils::GetValueForKey(json, UseAcrylicKey, _UseAcrylic);
|
||||
JsonUtils::GetValueForKey(json, SuppressApplicationTitleKey, _SuppressApplicationTitle);
|
||||
|
@ -392,6 +395,19 @@ void Profile::_FinalizeInheritance()
|
|||
}
|
||||
}
|
||||
}
|
||||
if (auto fontInfoImpl = get_self<FontConfig>(_FontInfo))
|
||||
{
|
||||
// Clear any existing parents first, we don't want duplicates from any previous
|
||||
// calls to this function
|
||||
fontInfoImpl->ClearParents();
|
||||
for (auto& parent : _parents)
|
||||
{
|
||||
if (auto parentFontInfoImpl = parent->_FontInfo.try_as<FontConfig>())
|
||||
{
|
||||
fontInfoImpl->InsertParent(parentFontInfoImpl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig Profile::DefaultAppearance()
|
||||
|
@ -399,6 +415,11 @@ winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig Profile::DefaultA
|
|||
return _DefaultAppearance;
|
||||
}
|
||||
|
||||
winrt::Microsoft::Terminal::Settings::Model::FontConfig Profile::FontInfo()
|
||||
{
|
||||
return _FontInfo;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Helper function for expanding any environment variables in a user-supplied starting directory and validating the resulting path
|
||||
// Arguments:
|
||||
|
@ -510,11 +531,8 @@ Json::Value Profile::ToJson() const
|
|||
JsonUtils::SetValueForKey(json, TabTitleKey, _TabTitle);
|
||||
|
||||
// Control Settings
|
||||
JsonUtils::SetValueForKey(json, FontWeightKey, _FontWeight);
|
||||
JsonUtils::SetValueForKey(json, ConnectionTypeKey, _ConnectionType);
|
||||
JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
|
||||
JsonUtils::SetValueForKey(json, FontFaceKey, _FontFace);
|
||||
JsonUtils::SetValueForKey(json, FontSizeKey, _FontSize);
|
||||
JsonUtils::SetValueForKey(json, AcrylicTransparencyKey, _AcrylicOpacity);
|
||||
JsonUtils::SetValueForKey(json, UseAcrylicKey, _UseAcrylic);
|
||||
JsonUtils::SetValueForKey(json, SuppressApplicationTitleKey, _SuppressApplicationTitle);
|
||||
|
@ -530,6 +548,13 @@ Json::Value Profile::ToJson() const
|
|||
JsonUtils::SetValueForKey(json, TabColorKey, _TabColor);
|
||||
JsonUtils::SetValueForKey(json, BellStyleKey, _BellStyle);
|
||||
|
||||
// Font settings
|
||||
const auto fontInfoImpl = winrt::get_self<FontConfig>(_FontInfo);
|
||||
if (fontInfoImpl->HasAnyOptionSet())
|
||||
{
|
||||
json[JsonKey(FontInfoKey)] = winrt::get_self<FontConfig>(_FontInfo)->ToJson();
|
||||
}
|
||||
|
||||
if (_UnfocusedAppearance)
|
||||
{
|
||||
json[JsonKey(UnfocusedAppearanceKey)] = winrt::get_self<AppearanceConfig>(_UnfocusedAppearance.value())->ToJson();
|
||||
|
|
|
@ -51,6 +51,7 @@ Author(s):
|
|||
#include "JsonUtils.h"
|
||||
#include <DefaultSettings.h>
|
||||
#include "AppearanceConfig.h"
|
||||
#include "FontConfig.h"
|
||||
|
||||
// fwdecl unittest classes
|
||||
namespace SettingsModelLocalTests
|
||||
|
@ -98,6 +99,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
static guid GetGuidOrGenerateForJson(const Json::Value& json) noexcept;
|
||||
|
||||
Model::IAppearanceConfig DefaultAppearance();
|
||||
Model::FontConfig FontInfo();
|
||||
|
||||
void _FinalizeInheritance() override;
|
||||
|
||||
|
@ -121,9 +123,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
INHERITABLE_SETTING(Model::Profile, double, AcrylicOpacity, 0.5);
|
||||
INHERITABLE_SETTING(Model::Profile, Microsoft::Terminal::Control::ScrollbarState, ScrollState, Microsoft::Terminal::Control::ScrollbarState::Visible);
|
||||
|
||||
INHERITABLE_SETTING(Model::Profile, hstring, FontFace, DEFAULT_FONT_FACE);
|
||||
INHERITABLE_SETTING(Model::Profile, int32_t, FontSize, DEFAULT_FONT_SIZE);
|
||||
INHERITABLE_SETTING(Model::Profile, Windows::UI::Text::FontWeight, FontWeight, DEFAULT_FONT_WEIGHT);
|
||||
INHERITABLE_SETTING(Model::Profile, hstring, Padding, DEFAULT_PADDING);
|
||||
|
||||
INHERITABLE_SETTING(Model::Profile, hstring, Commandline, L"cmd.exe");
|
||||
|
@ -143,6 +142,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
|
||||
private:
|
||||
Model::IAppearanceConfig _DefaultAppearance{ winrt::make<AppearanceConfig>(weak_ref<Model::Profile>(*this)) };
|
||||
Model::FontConfig _FontInfo{ winrt::make<FontConfig>(weak_ref<Model::Profile>(*this)) };
|
||||
static std::wstring EvaluateStartingDirectory(const std::wstring& directory);
|
||||
|
||||
static guid _GenerateGuidForProfile(const hstring& name, const hstring& source) noexcept;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT license.
|
||||
|
||||
import "IAppearanceConfig.idl";
|
||||
import "FontConfig.idl";
|
||||
#include "IInheritable.idl.h"
|
||||
|
||||
#define INHERITABLE_PROFILE_SETTING(Type, Name) \
|
||||
|
@ -62,15 +63,14 @@ namespace Microsoft.Terminal.Settings.Model
|
|||
INHERITABLE_PROFILE_SETTING(Boolean, UseAcrylic);
|
||||
INHERITABLE_PROFILE_SETTING(Double, AcrylicOpacity);
|
||||
INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.ScrollbarState, ScrollState);
|
||||
INHERITABLE_PROFILE_SETTING(String, FontFace);
|
||||
INHERITABLE_PROFILE_SETTING(Int32, FontSize);
|
||||
INHERITABLE_PROFILE_SETTING(Windows.UI.Text.FontWeight, FontWeight);
|
||||
INHERITABLE_PROFILE_SETTING(String, Padding);
|
||||
INHERITABLE_PROFILE_SETTING(String, Commandline);
|
||||
|
||||
INHERITABLE_PROFILE_SETTING(String, StartingDirectory);
|
||||
String EvaluatedStartingDirectory { get; };
|
||||
|
||||
FontConfig FontInfo { get; };
|
||||
|
||||
IAppearanceConfig DefaultAppearance { get; };
|
||||
INHERITABLE_PROFILE_SETTING(IAppearanceConfig, UnfocusedAppearance);
|
||||
|
||||
|
|
|
@ -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.
|
||||
-->
|
||||
|
@ -147,6 +147,10 @@
|
|||
<data name="ClosePaneCommandKey" xml:space="preserve">
|
||||
<value>Close pane</value>
|
||||
</data>
|
||||
<data name="CloseTabAtIndexCommandKey" xml:space="preserve">
|
||||
<value>Close tab at index {0}</value>
|
||||
<comment>{0} will be replaced with a number</comment>
|
||||
</data>
|
||||
<data name="CloseTabCommandKey" xml:space="preserve">
|
||||
<value>Close tab</value>
|
||||
</data>
|
||||
|
@ -413,4 +417,4 @@
|
|||
<value>Windows Console Host</value>
|
||||
<comment>Name describing the usage of the classic windows console as the terminal UI. (`conhost.exe`)</comment>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
|
@ -260,9 +260,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
_UseAcrylic = profile.UseAcrylic();
|
||||
_TintOpacity = profile.AcrylicOpacity();
|
||||
|
||||
_FontFace = profile.FontFace();
|
||||
_FontSize = profile.FontSize();
|
||||
_FontWeight = profile.FontWeight();
|
||||
_FontFace = profile.FontInfo().FontFace();
|
||||
_FontSize = profile.FontInfo().FontSize();
|
||||
_FontWeight = profile.FontInfo().FontWeight();
|
||||
_Padding = profile.Padding();
|
||||
|
||||
_Commandline = profile.Commandline();
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="ScopedResourceLoader.h" />
|
||||
<ClInclude Include="inc\LibraryResources.h" />
|
||||
<ClInclude Include="inc\ThrottledFunc.h" />
|
||||
<ClInclude Include="inc\Utils.h" />
|
||||
<ClInclude Include="inc\WtExeUtils.h" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="ScopedResourceLoader.h" />
|
||||
<ClInclude Include="inc\LibraryResources.h" />
|
||||
<ClInclude Include="inc\ThrottledFunc.h" />
|
||||
<ClInclude Include="inc\Utils.h" />
|
||||
<ClInclude Include="inc\WtExeUtils.h" />
|
||||
</ItemGroup>
|
||||
|
|
141
src/cascadia/WinRTUtils/inc/ThrottledFunc.h
Normal file
141
src/cascadia/WinRTUtils/inc/ThrottledFunc.h
Normal file
|
@ -0,0 +1,141 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "til/throttled_func.h"
|
||||
|
||||
// ThrottledFunc is a copy of til::throttled_func,
|
||||
// specialized for the use with a WinRT Dispatcher.
|
||||
template<bool leading, typename... Args>
|
||||
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<leading, Args...>>
|
||||
{
|
||||
public:
|
||||
using filetime_duration = std::chrono::duration<int64_t, std::ratio<1, 10000000>>;
|
||||
using function = std::function<void(Args...)>;
|
||||
|
||||
// Throttles invocations to the given `func` to not occur more often than `delay`.
|
||||
//
|
||||
// If this is a:
|
||||
// * ThrottledFuncLeading: `func` will be invoked immediately and
|
||||
// further invocations prevented until `delay` time has passed.
|
||||
// * ThrottledFuncTrailing: On the first invocation a timer of `delay` time will
|
||||
// be started. After the timer has expired `func` will be invoked just once.
|
||||
//
|
||||
// After `func` was invoked the state is reset and this cycle is repeated again.
|
||||
ThrottledFunc(
|
||||
winrt::Windows::UI::Core::CoreDispatcher dispatcher,
|
||||
filetime_duration delay,
|
||||
function func) :
|
||||
_dispatcher{ std::move(dispatcher) },
|
||||
_func{ std::move(func) },
|
||||
_timer{ _create_timer() }
|
||||
{
|
||||
const auto d = -delay.count();
|
||||
if (d >= 0)
|
||||
{
|
||||
throw std::invalid_argument("non-positive delay specified");
|
||||
}
|
||||
|
||||
memcpy(&_delay, &d, sizeof(d));
|
||||
}
|
||||
|
||||
// ThrottledFunc uses its `this` pointer when creating _timer.
|
||||
// Since the timer cannot be recreated, instances cannot be moved either.
|
||||
ThrottledFunc(const ThrottledFunc&) = delete;
|
||||
ThrottledFunc& operator=(const ThrottledFunc&) = delete;
|
||||
ThrottledFunc(ThrottledFunc&&) = delete;
|
||||
ThrottledFunc& operator=(ThrottledFunc&&) = delete;
|
||||
|
||||
// Throttles the invocation of the function passed to the constructor.
|
||||
// If this is a trailing_throttled_func:
|
||||
// If you call this function again before the underlying
|
||||
// timer has expired, the new arguments will be used.
|
||||
template<typename... MakeArgs>
|
||||
void Run(MakeArgs&&... args)
|
||||
{
|
||||
if (!_storage.emplace(std::forward<MakeArgs>(args)...))
|
||||
{
|
||||
_leading_edge();
|
||||
}
|
||||
}
|
||||
|
||||
// Modifies the pending arguments for the next function
|
||||
// invocation, if there is one pending currently.
|
||||
//
|
||||
// `func` will be invoked as func(Args...). Make sure to bind any
|
||||
// arguments in `func` by reference if you'd like to modify them.
|
||||
template<typename F>
|
||||
void ModifyPending(F func)
|
||||
{
|
||||
_storage.modify_pending(func);
|
||||
}
|
||||
|
||||
private:
|
||||
static void __stdcall _timer_callback(PTP_CALLBACK_INSTANCE /*instance*/, PVOID context, PTP_TIMER /*timer*/) noexcept
|
||||
{
|
||||
static_cast<ThrottledFunc*>(context)->_trailing_edge();
|
||||
}
|
||||
|
||||
void _leading_edge()
|
||||
{
|
||||
if constexpr (leading)
|
||||
{
|
||||
_dispatcher.RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakSelf = this->weak_from_this()]() {
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
try
|
||||
{
|
||||
self->_func();
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
SetThreadpoolTimerEx(self->_timer.get(), &self->_delay, 0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void _trailing_edge()
|
||||
{
|
||||
if constexpr (leading)
|
||||
{
|
||||
_storage.reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
_dispatcher.RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakSelf = this->weak_from_this()]() {
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
try
|
||||
{
|
||||
std::apply(self->_func, self->_storage.take());
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
inline wil::unique_threadpool_timer _create_timer()
|
||||
{
|
||||
wil::unique_threadpool_timer timer{ CreateThreadpoolTimer(&_timer_callback, this, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
FILETIME _delay;
|
||||
winrt::Windows::UI::Core::CoreDispatcher _dispatcher;
|
||||
function _func;
|
||||
|
||||
wil::unique_threadpool_timer _timer;
|
||||
til::details::throttled_func_storage<Args...> _storage;
|
||||
};
|
||||
|
||||
template<typename... Args>
|
||||
using ThrottledFuncTrailing = ThrottledFunc<false, Args...>;
|
||||
using ThrottledFuncLeading = ThrottledFunc<true>;
|
|
@ -103,10 +103,7 @@ ConsoleArguments::ConsoleArguments(const std::wstring& commandline,
|
|||
const HANDLE hStdOut) :
|
||||
_commandline(commandline),
|
||||
_vtInHandle(hStdIn),
|
||||
_vtOutHandle(hStdOut),
|
||||
_receivedEarlySizeChange{ false },
|
||||
_originalWidth{ -1 },
|
||||
_originalHeight{ -1 }
|
||||
_vtOutHandle(hStdOut)
|
||||
{
|
||||
_clientCommandline = L"";
|
||||
_vtMode = L"";
|
||||
|
@ -144,7 +141,6 @@ ConsoleArguments& ConsoleArguments::operator=(const ConsoleArguments& other)
|
|||
_width = other._width;
|
||||
_height = other._height;
|
||||
_inheritCursor = other._inheritCursor;
|
||||
_receivedEarlySizeChange = other._receivedEarlySizeChange;
|
||||
_runAsComServer = other._runAsComServer;
|
||||
_forceNoHandoff = other._forceNoHandoff;
|
||||
}
|
||||
|
@ -668,33 +664,6 @@ bool ConsoleArguments::IsWin32InputModeEnabled() const
|
|||
return _win32InputMode;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Tell us to use a different size than the one parsed as the size of the
|
||||
// console. This is called by the PtySignalInputThread when it receives a
|
||||
// resize before the first client has connected. Because there's no client,
|
||||
// there's also no buffer yet, so it has nothing to resize.
|
||||
// However, we shouldn't just discard that first resize message. Instead,
|
||||
// store it in here, so we can use the value when the first client does connect.
|
||||
// Arguments:
|
||||
// - dimensions: the new size in characters of the conpty buffer & viewport.
|
||||
// Return Value:
|
||||
// - <none>
|
||||
void ConsoleArguments::SetExpectedSize(COORD dimensions) noexcept
|
||||
{
|
||||
_width = dimensions.X;
|
||||
_height = dimensions.Y;
|
||||
// Stash away the original values we parsed when this is called.
|
||||
// This is to help debugging - if the signal thread DOES change these values,
|
||||
// we can still recover what was given to us on the commandline.
|
||||
if (!_receivedEarlySizeChange)
|
||||
{
|
||||
_originalWidth = _width;
|
||||
_originalHeight = _height;
|
||||
// Mark that we've changed size from what our commandline values were
|
||||
_receivedEarlySizeChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
// Method Description:
|
||||
// - This is a test helper method. It can be used to trick us into thinking
|
||||
|
|
|
@ -56,8 +56,6 @@ public:
|
|||
bool IsResizeQuirkEnabled() const;
|
||||
bool IsWin32InputModeEnabled() const;
|
||||
|
||||
void SetExpectedSize(COORD dimensions) noexcept;
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
void EnableConptyModeForTests();
|
||||
#endif
|
||||
|
@ -113,9 +111,6 @@ private:
|
|||
_signalHandle(signalHandle),
|
||||
_inheritCursor(inheritCursor),
|
||||
_resizeQuirk(false),
|
||||
_receivedEarlySizeChange{ false },
|
||||
_originalWidth{ -1 },
|
||||
_originalHeight{ -1 },
|
||||
_runAsComServer{ runAsComServer }
|
||||
{
|
||||
}
|
||||
|
@ -146,10 +141,6 @@ private:
|
|||
bool _resizeQuirk{ false };
|
||||
bool _win32InputMode{ false };
|
||||
|
||||
bool _receivedEarlySizeChange;
|
||||
short _originalWidth;
|
||||
short _originalHeight;
|
||||
|
||||
[[nodiscard]] HRESULT _GetClientCommandline(_Inout_ std::vector<std::wstring>& args,
|
||||
const size_t index,
|
||||
const bool skipFirst);
|
||||
|
|
|
@ -10,14 +10,6 @@
|
|||
#include "../interactivity/inc/ServiceLocator.hpp"
|
||||
#include "../terminal/adapter/DispatchCommon.hpp"
|
||||
|
||||
#define PTY_SIGNAL_RESIZE_WINDOW 8u
|
||||
|
||||
struct PTY_SIGNAL_RESIZE
|
||||
{
|
||||
unsigned short sx;
|
||||
unsigned short sy;
|
||||
};
|
||||
|
||||
using namespace Microsoft::Console;
|
||||
using namespace Microsoft::Console::Interactivity;
|
||||
using namespace Microsoft::Console::VirtualTerminal;
|
||||
|
@ -63,6 +55,8 @@ DWORD WINAPI PtySignalInputThread::StaticThreadProc(_In_ LPVOID lpParameter)
|
|||
// do something with the messages we receive now. Before this is set, there
|
||||
// is no guarantee that a client has attached, so most parts of the console
|
||||
// (in and screen buffers) haven't yet been initialized.
|
||||
// - NOTE: Call under LockConsole() to ensure other threads have an opportunity
|
||||
// to set early-work state.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
|
@ -70,6 +64,10 @@ DWORD WINAPI PtySignalInputThread::StaticThreadProc(_In_ LPVOID lpParameter)
|
|||
void PtySignalInputThread::ConnectConsole() noexcept
|
||||
{
|
||||
_consoleConnected = true;
|
||||
if (_earlyResize)
|
||||
{
|
||||
_DoResizeWindow(*_earlyResize);
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -79,38 +77,30 @@ void PtySignalInputThread::ConnectConsole() noexcept
|
|||
// - Otherwise it may cause an application termination and never return.
|
||||
[[nodiscard]] HRESULT PtySignalInputThread::_InputThread()
|
||||
{
|
||||
unsigned short signalId;
|
||||
PtySignal signalId;
|
||||
while (_GetData(&signalId, sizeof(signalId)))
|
||||
{
|
||||
switch (signalId)
|
||||
{
|
||||
case PTY_SIGNAL_RESIZE_WINDOW:
|
||||
case PtySignal::ResizeWindow:
|
||||
{
|
||||
PTY_SIGNAL_RESIZE resizeMsg = { 0 };
|
||||
ResizeWindowData resizeMsg = { 0 };
|
||||
_GetData(&resizeMsg, sizeof(resizeMsg));
|
||||
|
||||
LockConsole();
|
||||
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
|
||||
|
||||
// If the client app hasn't yet connected, stash the new size in the launchArgs.
|
||||
// We'll later use the value in launchArgs to set up the console buffer
|
||||
// We must be under lock here to ensure that someone else doesn't come in
|
||||
// and set with `ConnectConsole` while we're looking and modifying this.
|
||||
if (!_consoleConnected)
|
||||
{
|
||||
short sColumns = 0;
|
||||
short sRows = 0;
|
||||
if (SUCCEEDED(UShortToShort(resizeMsg.sx, &sColumns)) &&
|
||||
SUCCEEDED(UShortToShort(resizeMsg.sy, &sRows)) &&
|
||||
(sColumns > 0 && sRows > 0))
|
||||
{
|
||||
ServiceLocator::LocateGlobals().launchArgs.SetExpectedSize({ sColumns, sRows });
|
||||
}
|
||||
break;
|
||||
_earlyResize = resizeMsg;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (DispatchCommon::s_ResizeWindow(*_pConApi, resizeMsg.sx, resizeMsg.sy))
|
||||
{
|
||||
DispatchCommon::s_SuppressResizeRepaint(*_pConApi);
|
||||
}
|
||||
_DoResizeWindow(resizeMsg);
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -124,6 +114,20 @@ void PtySignalInputThread::ConnectConsole() noexcept
|
|||
return S_OK;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Dispatches a resize window message to the rest of the console code
|
||||
// Arguments:
|
||||
// - data - Packet information containing width/height (size) information
|
||||
// Return Value:
|
||||
// - <none>
|
||||
void PtySignalInputThread::_DoResizeWindow(const ResizeWindowData& data)
|
||||
{
|
||||
if (DispatchCommon::s_ResizeWindow(*_pConApi, data.sx, data.sy))
|
||||
{
|
||||
DispatchCommon::s_SuppressResizeRepaint(*_pConApi);
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Retrieves bytes from the file stream and exits or throws errors should the pipe state
|
||||
// be compromised.
|
||||
|
|
|
@ -33,14 +33,27 @@ namespace Microsoft::Console
|
|||
void ConnectConsole() noexcept;
|
||||
|
||||
private:
|
||||
enum class PtySignal : unsigned short
|
||||
{
|
||||
ResizeWindow = 8
|
||||
};
|
||||
|
||||
struct ResizeWindowData
|
||||
{
|
||||
unsigned short sx;
|
||||
unsigned short sy;
|
||||
};
|
||||
|
||||
[[nodiscard]] HRESULT _InputThread();
|
||||
bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer);
|
||||
void _DoResizeWindow(const ResizeWindowData& data);
|
||||
void _Shutdown();
|
||||
|
||||
wil::unique_hfile _hFile;
|
||||
wil::unique_handle _hThread;
|
||||
DWORD _dwThreadId;
|
||||
bool _consoleConnected;
|
||||
std::optional<ResizeWindowData> _earlyResize;
|
||||
std::unique_ptr<Microsoft::Console::VirtualTerminal::ConGetSet> _pConApi;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -76,8 +76,15 @@ void WriteBuffer::_DefaultStringCase(const std::wstring_view string)
|
|||
{
|
||||
size_t dwNumBytes = string.size() * sizeof(wchar_t);
|
||||
|
||||
_io.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsOn(true);
|
||||
Cursor& cursor = _io.GetActiveOutputBuffer().GetTextBuffer().GetCursor();
|
||||
if (!cursor.IsOn())
|
||||
{
|
||||
cursor.SetIsOn(true);
|
||||
}
|
||||
|
||||
// Defer the cursor drawing while we are iterating the string, for a better performance.
|
||||
// We can not waste time displaying a cursor event when we know more text is coming right behind it.
|
||||
cursor.StartDeferDrawing();
|
||||
_ntstatus = WriteCharsLegacy(_io.GetActiveOutputBuffer(),
|
||||
string.data(),
|
||||
string.data(),
|
||||
|
@ -87,6 +94,7 @@ void WriteBuffer::_DefaultStringCase(const std::wstring_view string)
|
|||
_io.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition().X,
|
||||
WC_LIMIT_BACKSPACE | WC_DELAY_EOL_WRAP,
|
||||
nullptr);
|
||||
cursor.EndDeferDrawing();
|
||||
}
|
||||
|
||||
ConhostInternalGetSet::ConhostInternalGetSet(_In_ IIoProvider& io) :
|
||||
|
|
86
src/inc/til/latch.h
Normal file
86
src/inc/til/latch.h
Normal file
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cpp_lib_latch
|
||||
#include <latch>
|
||||
#endif
|
||||
|
||||
namespace til
|
||||
{
|
||||
#ifdef __cpp_lib_latch
|
||||
using latch = std::latch;
|
||||
#else
|
||||
class latch
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] static constexpr ptrdiff_t max() noexcept
|
||||
{
|
||||
return std::numeric_limits<ptrdiff_t>::max();
|
||||
}
|
||||
|
||||
constexpr explicit latch(const ptrdiff_t expected) noexcept :
|
||||
counter{ expected }
|
||||
{
|
||||
assert(expected >= 0);
|
||||
}
|
||||
|
||||
latch(const latch&) = delete;
|
||||
latch& operator=(const latch&) = delete;
|
||||
|
||||
void count_down(const ptrdiff_t n = 1) noexcept
|
||||
{
|
||||
assert(n >= 0);
|
||||
|
||||
const auto old = counter.fetch_sub(n, std::memory_order_release);
|
||||
if (old == n)
|
||||
{
|
||||
WakeByAddressAll(&counter);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(old > n);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool try_wait() const noexcept
|
||||
{
|
||||
return counter.load(std::memory_order_acquire) == 0;
|
||||
}
|
||||
|
||||
void wait() const noexcept
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
auto current = counter.load(std::memory_order_acquire);
|
||||
if (current == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
assert(current > 0);
|
||||
WaitOnAddress(const_cast<decltype(counter)*>(&counter), ¤t, sizeof(counter), INFINITE);
|
||||
}
|
||||
}
|
||||
|
||||
void arrive_and_wait(const ptrdiff_t n = 1) noexcept
|
||||
{
|
||||
assert(n >= 0);
|
||||
|
||||
auto old = counter.fetch_sub(n, std::memory_order_acq_rel);
|
||||
if (old == n)
|
||||
{
|
||||
WakeByAddressAll(&counter);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(old > n);
|
||||
WaitOnAddress(&counter, &old, sizeof(counter), INFINITE);
|
||||
wait();
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<ptrdiff_t> counter;
|
||||
};
|
||||
#endif
|
||||
}
|
108
src/inc/til/mutex.h
Normal file
108
src/inc/til/mutex.h
Normal file
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace til
|
||||
{
|
||||
namespace details
|
||||
{
|
||||
template<typename T, typename Lock>
|
||||
class shared_mutex_guard
|
||||
{
|
||||
public:
|
||||
shared_mutex_guard(T& data, std::shared_mutex& mutex) :
|
||||
_data{ data },
|
||||
_lock{ mutex }
|
||||
{
|
||||
}
|
||||
|
||||
shared_mutex_guard(const shared_mutex_guard&) = delete;
|
||||
shared_mutex_guard& operator=(const shared_mutex_guard&) = delete;
|
||||
|
||||
shared_mutex_guard(shared_mutex_guard&&) = default;
|
||||
shared_mutex_guard& operator=(shared_mutex_guard&&) = default;
|
||||
|
||||
[[nodiscard]] constexpr T* operator->() const
|
||||
{
|
||||
return &_data;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr T& operator*() const&
|
||||
{
|
||||
return _data;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr T&& operator*() const&&
|
||||
{
|
||||
return std::move(_data);
|
||||
}
|
||||
|
||||
private:
|
||||
// We could reduce this to a single pointer member,
|
||||
// by storing a reference to the til::shared_mutex& class
|
||||
// and accessing its private members as a friend class.
|
||||
// But MSVC doesn't support strict aliasing. Nice!
|
||||
//
|
||||
// For instance if we had:
|
||||
// struct foo { int a, b; };
|
||||
// struct bar { foo& f; };
|
||||
//
|
||||
// void test(bar& b) {
|
||||
// b.f.a = 123;
|
||||
// b.f.b = 456;
|
||||
// }
|
||||
//
|
||||
// This would generate the following suboptimal assembly despite /O2:
|
||||
// mov rax, QWORD PTR [rcx]
|
||||
// mov DWORD PTR [rax], 123
|
||||
//
|
||||
// mov rax, QWORD PTR [rcx]
|
||||
// mov DWORD PTR [rax+4], 456
|
||||
T& _data;
|
||||
Lock _lock;
|
||||
};
|
||||
} // namespace details
|
||||
|
||||
// shared_mutex is a std::shared_mutex which also contains the data it's protecting.
|
||||
// It only allows access to the underlying data by locking the mutex and thus
|
||||
// ensures you don't forget to do so, unlike with std::mutex/std::shared_mutex.
|
||||
template<typename T>
|
||||
class shared_mutex
|
||||
{
|
||||
public:
|
||||
// An exclusive, read/write reference to a til::shared_mutex's underlying data.
|
||||
// If you drop the guard the mutex is unlocked.
|
||||
using guard = details::shared_mutex_guard<T, std::unique_lock<std::shared_mutex>>;
|
||||
|
||||
// A shared, read-only reference to a til::shared_mutex's underlying data.
|
||||
// If you drop the shared_guard the mutex is unlocked.
|
||||
using shared_guard = details::shared_mutex_guard<const T, std::shared_lock<std::shared_mutex>>;
|
||||
|
||||
shared_mutex() = default;
|
||||
|
||||
template<typename... Args>
|
||||
shared_mutex(Args&&... args) :
|
||||
_data{ std::forward<Args>(args)... }
|
||||
{
|
||||
}
|
||||
|
||||
// Acquire an exclusive, read/write reference to T.
|
||||
// For instance:
|
||||
// .lock()->foo = bar;
|
||||
[[nodiscard]] guard lock() const noexcept
|
||||
{
|
||||
return { _data, _mutex };
|
||||
}
|
||||
|
||||
// Acquire a shared, read-only reference to T.
|
||||
// For instance:
|
||||
// bar = .lock_shared()->foo;
|
||||
[[nodiscard]] shared_guard lock_shared() const noexcept
|
||||
{
|
||||
return { _data, _mutex };
|
||||
}
|
||||
|
||||
private:
|
||||
mutable T _data{};
|
||||
mutable std::shared_mutex _mutex;
|
||||
};
|
||||
} // namespace til
|
|
@ -48,4 +48,26 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
|
|||
{
|
||||
return starts_with<>(str, prefix);
|
||||
};
|
||||
|
||||
template<typename T, typename Traits>
|
||||
constexpr bool ends_with(const std::basic_string_view<T, Traits> str, const std::basic_string_view<T, Traits> prefix) noexcept
|
||||
{
|
||||
#ifdef __cpp_lib_ends_ends_with
|
||||
#error This code can be replaced in C++20, which natively supports .ends_with().
|
||||
#endif
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
|
||||
return str.size() >= prefix.size() && Traits::compare(str.data() + (str.size() - prefix.size()), prefix.data(), prefix.size()) == 0;
|
||||
#pragma warning(pop)
|
||||
};
|
||||
|
||||
constexpr bool ends_with(const std::string_view str, const std::string_view prefix) noexcept
|
||||
{
|
||||
return ends_with<>(str, prefix);
|
||||
};
|
||||
|
||||
constexpr bool ends_with(const std::wstring_view str, const std::wstring_view prefix) noexcept
|
||||
{
|
||||
return ends_with<>(str, prefix);
|
||||
};
|
||||
}
|
||||
|
|
206
src/inc/til/throttled_func.h
Normal file
206
src/inc/til/throttled_func.h
Normal file
|
@ -0,0 +1,206 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace til
|
||||
{
|
||||
namespace details
|
||||
{
|
||||
template<typename... Args>
|
||||
class throttled_func_storage
|
||||
{
|
||||
public:
|
||||
template<typename... MakeArgs>
|
||||
bool emplace(MakeArgs&&... args)
|
||||
{
|
||||
std::unique_lock guard{ _lock };
|
||||
const bool hadValue = _pendingRunArgs.has_value();
|
||||
_pendingRunArgs.emplace(std::forward<MakeArgs>(args)...);
|
||||
return hadValue;
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void modify_pending(F f)
|
||||
{
|
||||
std::unique_lock guard{ _lock };
|
||||
if (_pendingRunArgs)
|
||||
{
|
||||
std::apply(f, *_pendingRunArgs);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<Args...> take()
|
||||
{
|
||||
std::unique_lock guard{ _lock };
|
||||
auto pendingRunArgs = std::move(*_pendingRunArgs);
|
||||
_pendingRunArgs.reset();
|
||||
return pendingRunArgs;
|
||||
}
|
||||
|
||||
explicit operator bool() const
|
||||
{
|
||||
std::shared_lock guard{ _lock };
|
||||
return _pendingRunArgs.has_value();
|
||||
}
|
||||
|
||||
private:
|
||||
// std::mutex uses imperfect Critical Sections on Windows.
|
||||
// --> std::shared_mutex uses SRW locks that are small and fast.
|
||||
mutable std::shared_mutex _lock;
|
||||
std::optional<std::tuple<Args...>> _pendingRunArgs;
|
||||
};
|
||||
|
||||
template<>
|
||||
class throttled_func_storage<>
|
||||
{
|
||||
public:
|
||||
bool emplace()
|
||||
{
|
||||
return _isPending.exchange(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::tuple<> take()
|
||||
{
|
||||
reset();
|
||||
return {};
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
_isPending.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
explicit operator bool() const
|
||||
{
|
||||
return _isPending.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<bool> _isPending;
|
||||
};
|
||||
} // namespace details
|
||||
|
||||
template<bool leading, typename... Args>
|
||||
class throttled_func
|
||||
{
|
||||
public:
|
||||
using filetime_duration = std::chrono::duration<int64_t, std::ratio<1, 10000000>>;
|
||||
using function = std::function<void(Args...)>;
|
||||
|
||||
// Throttles invocations to the given `func` to not occur more often than `delay`.
|
||||
//
|
||||
// If this is a:
|
||||
// * throttled_func_leading: `func` will be invoked immediately and
|
||||
// further invocations prevented until `delay` time has passed.
|
||||
// * throttled_func_trailing: On the first invocation a timer of `delay` time will
|
||||
// be started. After the timer has expired `func` will be invoked just once.
|
||||
//
|
||||
// After `func` was invoked the state is reset and this cycle is repeated again.
|
||||
throttled_func(filetime_duration delay, function func) :
|
||||
_func{ std::move(func) },
|
||||
_timer{ _createTimer() }
|
||||
{
|
||||
const auto d = -delay.count();
|
||||
if (d >= 0)
|
||||
{
|
||||
throw std::invalid_argument("non-positive delay specified");
|
||||
}
|
||||
|
||||
memcpy(&_delay, &d, sizeof(d));
|
||||
}
|
||||
|
||||
// throttled_func uses its `this` pointer when creating _timer.
|
||||
// Since the timer cannot be recreated, instances cannot be moved either.
|
||||
throttled_func(const throttled_func&) = delete;
|
||||
throttled_func& operator=(const throttled_func&) = delete;
|
||||
throttled_func(throttled_func&&) = delete;
|
||||
throttled_func& operator=(throttled_func&&) = delete;
|
||||
|
||||
// Throttles the invocation of the function passed to the constructor.
|
||||
// If this is a trailing_throttled_func:
|
||||
// If you call this function again before the underlying
|
||||
// timer has expired, the new arguments will be used.
|
||||
template<typename... MakeArgs>
|
||||
void operator()(MakeArgs&&... args)
|
||||
{
|
||||
if (!_storage.emplace(std::forward<MakeArgs>(args)...))
|
||||
{
|
||||
_leading_edge();
|
||||
}
|
||||
}
|
||||
|
||||
// Modifies the pending arguments for the next function
|
||||
// invocation, if there is one pending currently.
|
||||
//
|
||||
// `func` will be invoked as func(Args...). Make sure to bind any
|
||||
// arguments in `func` by reference if you'd like to modify them.
|
||||
template<typename F>
|
||||
void modify_pending(F func)
|
||||
{
|
||||
_storage.modify_pending(func);
|
||||
}
|
||||
|
||||
// Makes sure that the currently pending timer is executed
|
||||
// as soon as possible and in that case waits for its completion.
|
||||
// You can use this function in your destructor to ensure that any
|
||||
// pending callback invocation is completed as soon as possible.
|
||||
//
|
||||
// NOTE: Don't call this function if the operator()
|
||||
// could still be called concurrently.
|
||||
void flush()
|
||||
{
|
||||
WaitForThreadpoolTimerCallbacks(_timer.get(), true);
|
||||
if (_storage)
|
||||
{
|
||||
_trailing_edge();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static void __stdcall _timer_callback(PTP_CALLBACK_INSTANCE /*instance*/, PVOID context, PTP_TIMER /*timer*/) noexcept
|
||||
try
|
||||
{
|
||||
static_cast<throttled_func*>(context)->_trailing_edge();
|
||||
}
|
||||
CATCH_LOG()
|
||||
|
||||
void _leading_edge()
|
||||
{
|
||||
if constexpr (leading)
|
||||
{
|
||||
_func();
|
||||
}
|
||||
|
||||
SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
|
||||
}
|
||||
|
||||
void _trailing_edge()
|
||||
{
|
||||
if constexpr (leading)
|
||||
{
|
||||
_storage.reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
std::apply(_func, _storage.take());
|
||||
}
|
||||
}
|
||||
|
||||
inline wil::unique_threadpool_timer _createTimer()
|
||||
{
|
||||
wil::unique_threadpool_timer timer{ CreateThreadpoolTimer(&_timer_callback, this, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
FILETIME _delay;
|
||||
function _func;
|
||||
wil::unique_threadpool_timer _timer;
|
||||
details::throttled_func_storage<Args...> _storage;
|
||||
};
|
||||
|
||||
template<typename... Args>
|
||||
using throttled_func_trailing = throttled_func<false, Args...>;
|
||||
using throttled_func_leading = throttled_func<true>;
|
||||
} // namespace til
|
|
@ -133,10 +133,21 @@ CATCH_RETURN()
|
|||
_In_ IDWriteTextRenderer* renderer,
|
||||
FLOAT originX,
|
||||
FLOAT originY) noexcept
|
||||
try
|
||||
{
|
||||
const auto drawingContext = static_cast<const DrawingContext*>(clientDrawingContext);
|
||||
_formatInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicTextFormat().Get() : _fontRenderData->DefaultTextFormat().Get();
|
||||
_fontInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicFontFace().Get() : _fontRenderData->DefaultFontFace().Get();
|
||||
|
||||
const DWRITE_FONT_WEIGHT weight = _fontRenderData->DefaultFontWeight();
|
||||
DWRITE_FONT_STYLE style = _fontRenderData->DefaultFontStyle();
|
||||
const DWRITE_FONT_STRETCH stretch = _fontRenderData->DefaultFontStretch();
|
||||
|
||||
if (drawingContext->useItalicFont)
|
||||
{
|
||||
style = DWRITE_FONT_STYLE_ITALIC;
|
||||
}
|
||||
|
||||
_formatInUse = _fontRenderData->TextFormatWithAttribute(weight, style, stretch).Get();
|
||||
_fontInUse = _fontRenderData->FontFaceWithAttribute(weight, style, stretch).Get();
|
||||
|
||||
RETURN_IF_FAILED(_AnalyzeTextComplexity());
|
||||
RETURN_IF_FAILED(_AnalyzeRuns());
|
||||
|
@ -151,6 +162,7 @@ CATCH_RETURN()
|
|||
|
||||
return S_OK;
|
||||
}
|
||||
CATCH_RETURN()
|
||||
|
||||
// Routine Description:
|
||||
// - Uses the internal text information and the analyzers/font information from construction
|
||||
|
|
405
src/renderer/dx/DxFontInfo.cpp
Normal file
405
src/renderer/dx/DxFontInfo.cpp
Normal file
|
@ -0,0 +1,405 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "DxFontInfo.h"
|
||||
|
||||
#include "unicode.hpp"
|
||||
|
||||
#include <VersionHelpers.h>
|
||||
|
||||
static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" };
|
||||
|
||||
using namespace Microsoft::Console::Render;
|
||||
|
||||
DxFontInfo::DxFontInfo() noexcept :
|
||||
_familyName(),
|
||||
_weight(DWRITE_FONT_WEIGHT_NORMAL),
|
||||
_style(DWRITE_FONT_STYLE_NORMAL),
|
||||
_stretch(DWRITE_FONT_STRETCH_NORMAL),
|
||||
_didFallback(false)
|
||||
{
|
||||
}
|
||||
|
||||
DxFontInfo::DxFontInfo(std::wstring_view familyName,
|
||||
unsigned int weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch) :
|
||||
DxFontInfo(familyName, static_cast<DWRITE_FONT_WEIGHT>(weight), style, stretch)
|
||||
{
|
||||
}
|
||||
|
||||
DxFontInfo::DxFontInfo(std::wstring_view familyName,
|
||||
DWRITE_FONT_WEIGHT weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch) :
|
||||
_familyName(familyName),
|
||||
_weight(weight),
|
||||
_style(style),
|
||||
_stretch(stretch),
|
||||
_didFallback(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool DxFontInfo::operator==(const DxFontInfo& other) const noexcept
|
||||
{
|
||||
return (_familyName == other._familyName &&
|
||||
_weight == other._weight &&
|
||||
_style == other._style &&
|
||||
_stretch == other._stretch &&
|
||||
_didFallback == other._didFallback);
|
||||
}
|
||||
|
||||
std::wstring_view DxFontInfo::GetFamilyName() const noexcept
|
||||
{
|
||||
return _familyName;
|
||||
}
|
||||
|
||||
void DxFontInfo::SetFamilyName(const std::wstring_view familyName)
|
||||
{
|
||||
_familyName = familyName;
|
||||
}
|
||||
|
||||
DWRITE_FONT_WEIGHT DxFontInfo::GetWeight() const noexcept
|
||||
{
|
||||
return _weight;
|
||||
}
|
||||
|
||||
void DxFontInfo::SetWeight(const DWRITE_FONT_WEIGHT weight) noexcept
|
||||
{
|
||||
_weight = weight;
|
||||
}
|
||||
|
||||
DWRITE_FONT_STYLE DxFontInfo::GetStyle() const noexcept
|
||||
{
|
||||
return _style;
|
||||
}
|
||||
|
||||
void DxFontInfo::SetStyle(const DWRITE_FONT_STYLE style) noexcept
|
||||
{
|
||||
_style = style;
|
||||
}
|
||||
|
||||
DWRITE_FONT_STRETCH DxFontInfo::GetStretch() const noexcept
|
||||
{
|
||||
return _stretch;
|
||||
}
|
||||
|
||||
void DxFontInfo::SetStretch(const DWRITE_FONT_STRETCH stretch) noexcept
|
||||
{
|
||||
_stretch = stretch;
|
||||
}
|
||||
|
||||
bool DxFontInfo::GetFallback() const noexcept
|
||||
{
|
||||
return _didFallback;
|
||||
}
|
||||
|
||||
void DxFontInfo::SetFromEngine(const std::wstring_view familyName,
|
||||
const DWRITE_FONT_WEIGHT weight,
|
||||
const DWRITE_FONT_STYLE style,
|
||||
const DWRITE_FONT_STRETCH stretch)
|
||||
{
|
||||
_familyName = familyName;
|
||||
_weight = weight;
|
||||
_style = style;
|
||||
_stretch = stretch;
|
||||
}
|
||||
|
||||
// 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:
|
||||
// - dwriteFactory - The DWrite factory to use
|
||||
// - localeName - Locale to search for appropriate fonts
|
||||
// Return Value:
|
||||
// - Smart pointer holding interface reference for queryable font data.
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontInfo::ResolveFontFaceWithFallback(gsl::not_null<IDWriteFactory1*> dwriteFactory,
|
||||
std::wstring& localeName)
|
||||
{
|
||||
// First attempt to find exactly what the user asked for.
|
||||
_didFallback = false;
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace1> face{ nullptr };
|
||||
|
||||
// GH#10211 - wrap this all up in a try/catch. If the nearby fonts are
|
||||
// corrupted, then we don't want to throw out of this top half of this
|
||||
// method. We still want to fall back to a font that's reasonable, below.
|
||||
try
|
||||
{
|
||||
face = _FindFontFace(dwriteFactory, localeName, true);
|
||||
|
||||
if (!face)
|
||||
{
|
||||
// If we missed, try looking a little more by trimming the last word off the requested family name a few times.
|
||||
// Quite often, folks are specifying weights or something in the familyName and it causes failed resolution and
|
||||
// an unexpected error dialog. We theoretically could detect the weight words and convert them, but this
|
||||
// is the quick fix for the majority scenario.
|
||||
// The long/full fix is backlogged to GH#9744
|
||||
// Also this doesn't count as a fallback because we don't want to annoy folks with the warning dialog over
|
||||
// this resolution.
|
||||
while (!face && !_familyName.empty())
|
||||
{
|
||||
const auto lastSpace = _familyName.find_last_of(UNICODE_SPACE);
|
||||
|
||||
// value is unsigned and npos will be greater than size.
|
||||
// if we didn't find anything to trim, leave.
|
||||
if (lastSpace >= _familyName.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// trim string down to just before the found space
|
||||
// (space found at 6... trim from 0 for 6 length will give us 0-5 as the new string)
|
||||
_familyName = _familyName.substr(0, lastSpace);
|
||||
|
||||
// Try to find it with the shortened family name
|
||||
face = _FindFontFace(dwriteFactory, localeName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
// Alright, if our quick shot at trimming didn't work either...
|
||||
// move onto looking up a font from our hardcoded list of fonts
|
||||
// that should really always be available.
|
||||
if (!face)
|
||||
{
|
||||
for (const auto fallbackFace : FALLBACK_FONT_FACES)
|
||||
{
|
||||
_familyName = fallbackFace;
|
||||
// With these fonts, don't attempt the nearby lookup. We're looking
|
||||
// for system fonts only. If one of the nearby fonts is causing us
|
||||
// problems (like in GH#10211), then we don't want to go anywhere
|
||||
|
||||
// near it in this part.
|
||||
face = _FindFontFace(dwriteFactory, localeName, false);
|
||||
|
||||
if (face)
|
||||
{
|
||||
_didFallback = true;
|
||||
break;
|
||||
}
|
||||
|
||||
_familyName = fallbackFace;
|
||||
_weight = DWRITE_FONT_WEIGHT_NORMAL;
|
||||
_stretch = DWRITE_FONT_STRETCH_NORMAL;
|
||||
_style = DWRITE_FONT_STYLE_NORMAL;
|
||||
face = _FindFontFace(dwriteFactory, localeName, false);
|
||||
|
||||
if (face)
|
||||
{
|
||||
_didFallback = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
THROW_HR_IF_NULL(E_FAIL, face);
|
||||
|
||||
return face;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Locates a suitable font face from the given information
|
||||
// Arguments:
|
||||
// - dwriteFactory - The DWrite factory to use
|
||||
// - localeName - Locale to search for appropriate fonts
|
||||
// Return Value:
|
||||
// - Smart pointer holding interface reference for queryable font data.
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontInfo::_FindFontFace(gsl::not_null<IDWriteFactory1*> dwriteFactory, std::wstring& localeName, const bool withNearbyLookup)
|
||||
{
|
||||
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 the system collection missed, try the files sitting next to our binary.
|
||||
if (withNearbyLookup && !familyExists)
|
||||
{
|
||||
auto&& nearbyCollection = _NearbyCollection(dwriteFactory);
|
||||
|
||||
// May be null on OS below Windows 10. If null, just skip the attempt.
|
||||
if (nearbyCollection)
|
||||
{
|
||||
nearbyCollection.As(&fontCollection);
|
||||
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(GetWeight(), GetStretch(), GetStyle(), &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 DxFontInfo::_GetFontFamilyName(gsl::not_null<IDWriteFontFamily*> const fontFamily,
|
||||
std::wstring& localeName)
|
||||
{
|
||||
// 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 = L"en-us";
|
||||
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:
|
||||
// - Creates a DirectWrite font collection of font files that are sitting next to the running
|
||||
// binary (in the same directory as the EXE).
|
||||
// Arguments:
|
||||
// - dwriteFactory - The DWrite factory to use
|
||||
// Return Value:
|
||||
// - DirectWrite font collection. May be null if one cannot be created.
|
||||
[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& DxFontInfo::_NearbyCollection(gsl::not_null<IDWriteFactory1*> dwriteFactory) const
|
||||
{
|
||||
// Magic static so we only attempt to grovel the hard disk once no matter how many instances
|
||||
// of the font collection itself we require.
|
||||
static const auto knownPaths = s_GetNearbyFonts();
|
||||
|
||||
// The convenience interfaces for loading fonts from files
|
||||
// are only available on Windows 10+.
|
||||
// Don't try to look up if below that OS version.
|
||||
static const bool s_isWindows10OrGreater = IsWindows10OrGreater();
|
||||
|
||||
if (s_isWindows10OrGreater && !_nearbyCollection)
|
||||
{
|
||||
// Factory3 has a convenience to get us a font set builder.
|
||||
::Microsoft::WRL::ComPtr<IDWriteFactory3> factory3;
|
||||
THROW_IF_FAILED(dwriteFactory->QueryInterface<IDWriteFactory3>(&factory3));
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder> fontSetBuilder;
|
||||
THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder));
|
||||
|
||||
// Builder2 has a convenience to just feed in paths to font files.
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder2> fontSetBuilder2;
|
||||
THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2));
|
||||
|
||||
for (auto& p : knownPaths)
|
||||
{
|
||||
fontSetBuilder2->AddFontFile(p.c_str());
|
||||
}
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSet> fontSet;
|
||||
THROW_IF_FAILED(fontSetBuilder2->CreateFontSet(&fontSet));
|
||||
|
||||
THROW_IF_FAILED(factory3->CreateFontCollectionFromFontSet(fontSet.Get(), &_nearbyCollection));
|
||||
}
|
||||
|
||||
return _nearbyCollection;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Digs through the directory that the current executable is running within to find
|
||||
// any TTF files sitting next to it.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - Iterable collection of filesystem paths, one per font file that was found
|
||||
[[nodiscard]] std::vector<std::filesystem::path> DxFontInfo::s_GetNearbyFonts()
|
||||
{
|
||||
std::vector<std::filesystem::path> paths;
|
||||
|
||||
// Find the directory we're running from then enumerate all the TTF files
|
||||
// sitting next to us.
|
||||
const std::filesystem::path module{ wil::GetModuleFileNameW<std::wstring>(nullptr) };
|
||||
const auto folder{ module.parent_path() };
|
||||
|
||||
for (auto& p : std::filesystem::directory_iterator(folder))
|
||||
{
|
||||
if (p.is_regular_file())
|
||||
{
|
||||
auto extension = p.path().extension().wstring();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), std::towlower);
|
||||
|
||||
static constexpr std::wstring_view ttfExtension{ L".ttf" };
|
||||
if (ttfExtension == extension)
|
||||
{
|
||||
paths.push_back(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
108
src/renderer/dx/DxFontInfo.h
Normal file
108
src/renderer/dx/DxFontInfo.h
Normal file
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <dwrite.h>
|
||||
#include <dwrite_1.h>
|
||||
#include <dwrite_2.h>
|
||||
#include <dwrite_3.h>
|
||||
|
||||
namespace Microsoft::Console::Render
|
||||
{
|
||||
class DxFontInfo
|
||||
{
|
||||
public:
|
||||
DxFontInfo() noexcept;
|
||||
|
||||
DxFontInfo(std::wstring_view familyName,
|
||||
unsigned int weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch);
|
||||
|
||||
DxFontInfo(std::wstring_view familyName,
|
||||
DWRITE_FONT_WEIGHT weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch);
|
||||
|
||||
bool operator==(const DxFontInfo& other) const noexcept;
|
||||
|
||||
std::wstring_view GetFamilyName() const noexcept;
|
||||
void SetFamilyName(const std::wstring_view familyName);
|
||||
|
||||
DWRITE_FONT_WEIGHT GetWeight() const noexcept;
|
||||
void SetWeight(const DWRITE_FONT_WEIGHT weight) noexcept;
|
||||
|
||||
DWRITE_FONT_STYLE GetStyle() const noexcept;
|
||||
void SetStyle(const DWRITE_FONT_STYLE style) noexcept;
|
||||
|
||||
DWRITE_FONT_STRETCH GetStretch() const noexcept;
|
||||
void SetStretch(const DWRITE_FONT_STRETCH stretch) noexcept;
|
||||
|
||||
bool GetFallback() const noexcept;
|
||||
|
||||
void SetFromEngine(const std::wstring_view familyName,
|
||||
const DWRITE_FONT_WEIGHT weight,
|
||||
const DWRITE_FONT_STYLE style,
|
||||
const DWRITE_FONT_STRETCH stretch);
|
||||
|
||||
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> ResolveFontFaceWithFallback(gsl::not_null<IDWriteFactory1*> dwriteFactory,
|
||||
std::wstring& localeName);
|
||||
|
||||
private:
|
||||
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _FindFontFace(gsl::not_null<IDWriteFactory1*> dwriteFactory,
|
||||
std::wstring& localeName,
|
||||
const bool withNearbyLookup);
|
||||
|
||||
[[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null<IDWriteFontFamily*> const fontFamily,
|
||||
std::wstring& localeName);
|
||||
|
||||
[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& _NearbyCollection(gsl::not_null<IDWriteFactory1*> dwriteFactory) const;
|
||||
|
||||
[[nodiscard]] static std::vector<std::filesystem::path> s_GetNearbyFonts();
|
||||
|
||||
mutable ::Microsoft::WRL::ComPtr<IDWriteFontCollection1> _nearbyCollection;
|
||||
|
||||
// The font name we should be looking for
|
||||
std::wstring _familyName;
|
||||
|
||||
// The weight (bold, light, etc.)
|
||||
DWRITE_FONT_WEIGHT _weight;
|
||||
|
||||
// Normal, italic, etc.
|
||||
DWRITE_FONT_STYLE _style;
|
||||
|
||||
// The stretch of the font is the spacing between each letter
|
||||
DWRITE_FONT_STRETCH _stretch;
|
||||
|
||||
// Indicates whether we couldn't match the user request and had to choose from a hardcoded default list.
|
||||
bool _didFallback;
|
||||
};
|
||||
}
|
||||
|
||||
namespace std
|
||||
{
|
||||
template<>
|
||||
struct hash<Microsoft::Console::Render::DxFontInfo>
|
||||
{
|
||||
size_t operator()(const Microsoft::Console::Render::DxFontInfo& fontInfo) const noexcept
|
||||
{
|
||||
const size_t h1 = std::hash<std::wstring_view>{}(fontInfo.GetFamilyName());
|
||||
const size_t h2 = std::hash<DWRITE_FONT_WEIGHT>{}(fontInfo.GetWeight());
|
||||
const size_t h3 = std::hash<DWRITE_FONT_STYLE>{}(fontInfo.GetStyle());
|
||||
const size_t h4 = std::hash<DWRITE_FONT_STRETCH>{}(fontInfo.GetStretch());
|
||||
const size_t h5 = std::hash<bool>{}(fontInfo.GetFallback());
|
||||
|
||||
static const auto combine = [](std::initializer_list<size_t> list) {
|
||||
size_t seed = 0;
|
||||
for (auto hash : list)
|
||||
{
|
||||
seed ^= hash + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
return seed;
|
||||
};
|
||||
|
||||
return combine({ h1, h2, h3, h4, h5 });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -17,14 +17,22 @@ using namespace Microsoft::Console::Render;
|
|||
|
||||
DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory) noexcept :
|
||||
_dwriteFactory(dwriteFactory),
|
||||
_fontSize{},
|
||||
_glyphCell{},
|
||||
_lineMetrics({}),
|
||||
_boxDrawingEffect{}
|
||||
_lineMetrics{},
|
||||
_lineSpacing{}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> DxFontRenderData::Analyzer() noexcept
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> DxFontRenderData::Analyzer()
|
||||
{
|
||||
if (!_dwriteTextAnalyzer)
|
||||
{
|
||||
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer> analyzer;
|
||||
THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
|
||||
THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer));
|
||||
}
|
||||
|
||||
return _dwriteTextAnalyzer;
|
||||
}
|
||||
|
||||
|
@ -40,49 +48,24 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwr
|
|||
return _systemFontFallback;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Creates a DirectWrite font collection of font files that are sitting next to the running
|
||||
// binary (in the same directory as the EXE).
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - DirectWrite font collection. May be null if one cannot be created.
|
||||
[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& DxFontRenderData::NearbyCollection() const
|
||||
[[nodiscard]] std::wstring DxFontRenderData::UserLocaleName()
|
||||
{
|
||||
// Magic static so we only attempt to grovel the hard disk once no matter how many instances
|
||||
// of the font collection itself we require.
|
||||
static const auto knownPaths = s_GetNearbyFonts();
|
||||
|
||||
// The convenience interfaces for loading fonts from files
|
||||
// are only available on Windows 10+.
|
||||
// Don't try to look up if below that OS version.
|
||||
static const bool s_isWindows10OrGreater = IsWindows10OrGreater();
|
||||
|
||||
if (s_isWindows10OrGreater && !_nearbyCollection)
|
||||
if (_userLocaleName.empty())
|
||||
{
|
||||
// Factory3 has a convenience to get us a font set builder.
|
||||
::Microsoft::WRL::ComPtr<IDWriteFactory3> factory3;
|
||||
THROW_IF_FAILED(_dwriteFactory.As(&factory3));
|
||||
std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> localeName;
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder> fontSetBuilder;
|
||||
THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder));
|
||||
|
||||
// Builder2 has a convenience to just feed in paths to font files.
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder2> fontSetBuilder2;
|
||||
THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2));
|
||||
|
||||
for (auto& p : knownPaths)
|
||||
const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow<int>(localeName.size()));
|
||||
if (returnCode)
|
||||
{
|
||||
fontSetBuilder2->AddFontFile(p.c_str());
|
||||
_userLocaleName = { localeName.data() };
|
||||
}
|
||||
else
|
||||
{
|
||||
_userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
|
||||
}
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSet> fontSet;
|
||||
THROW_IF_FAILED(fontSetBuilder2->CreateFontSet(&fontSet));
|
||||
|
||||
THROW_IF_FAILED(factory3->CreateFontCollectionFromFontSet(fontSet.Get(), &_nearbyCollection));
|
||||
}
|
||||
|
||||
return _nearbyCollection;
|
||||
return _userLocaleName;
|
||||
}
|
||||
|
||||
[[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept
|
||||
|
@ -95,29 +78,96 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwr
|
|||
return _lineMetrics;
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::DefaultTextFormat() noexcept
|
||||
[[nodiscard]] DWRITE_FONT_WEIGHT DxFontRenderData::DefaultFontWeight() noexcept
|
||||
{
|
||||
return _dwriteTextFormat;
|
||||
return _defaultFontInfo.GetWeight();
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::DefaultFontFace() noexcept
|
||||
[[nodiscard]] DWRITE_FONT_STYLE DxFontRenderData::DefaultFontStyle() noexcept
|
||||
{
|
||||
return _dwriteFontFace;
|
||||
return _defaultFontInfo.GetStyle();
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DxFontRenderData::DefaultBoxDrawingEffect() noexcept
|
||||
[[nodiscard]] DWRITE_FONT_STRETCH DxFontRenderData::DefaultFontStretch() noexcept
|
||||
{
|
||||
return _defaultFontInfo.GetStretch();
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::DefaultTextFormat()
|
||||
{
|
||||
return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch());
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::DefaultFontFace()
|
||||
{
|
||||
return FontFaceWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch());
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DxFontRenderData::DefaultBoxDrawingEffect()
|
||||
{
|
||||
if (!_boxDrawingEffect)
|
||||
{
|
||||
// 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.
|
||||
THROW_IF_FAILED(s_CalculateBoxEffect(DefaultTextFormat().Get(), _glyphCell.width(), DefaultFontFace().Get(), 1.0f, &_boxDrawingEffect));
|
||||
}
|
||||
|
||||
return _boxDrawingEffect;
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::ItalicTextFormat() noexcept
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch)
|
||||
{
|
||||
return _dwriteTextFormatItalic;
|
||||
DxFontInfo fontInfo = _defaultFontInfo;
|
||||
fontInfo.SetWeight(weight);
|
||||
fontInfo.SetStyle(style);
|
||||
fontInfo.SetStretch(stretch);
|
||||
|
||||
const auto textFormatIt = _textFormatMap.find(fontInfo);
|
||||
if (textFormatIt == _textFormatMap.end())
|
||||
{
|
||||
// Create the font with the fractional pixel height size.
|
||||
// It should have an integer pixel width by our math.
|
||||
// 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.
|
||||
std::wstring localeName = UserLocaleName();
|
||||
Microsoft::WRL::ComPtr<IDWriteTextFormat> textFormat;
|
||||
THROW_IF_FAILED(_BuildTextFormat(fontInfo, localeName).As(&textFormat));
|
||||
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));
|
||||
|
||||
_textFormatMap.insert({ fontInfo, textFormat });
|
||||
return textFormat;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (*textFormatIt).second;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::ItalicFontFace() noexcept
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch)
|
||||
{
|
||||
return _dwriteFontFaceItalic;
|
||||
DxFontInfo fontInfo = _defaultFontInfo;
|
||||
fontInfo.SetWeight(weight);
|
||||
fontInfo.SetStyle(style);
|
||||
fontInfo.SetStretch(stretch);
|
||||
|
||||
const auto fontFaceIt = _fontFaceMap.find(fontInfo);
|
||||
if (fontFaceIt == _fontFaceMap.end())
|
||||
{
|
||||
std::wstring fontLocaleName = UserLocaleName();
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace = fontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName);
|
||||
|
||||
_fontFaceMap.insert({ fontInfo, fontFace });
|
||||
return fontFace;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (*fontFaceIt).second;
|
||||
}
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
|
@ -133,247 +183,17 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwr
|
|||
try
|
||||
{
|
||||
_userLocaleName.clear();
|
||||
_textFormatMap.clear();
|
||||
_fontFaceMap.clear();
|
||||
_boxDrawingEffect.Reset();
|
||||
|
||||
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();
|
||||
// Initialize the default font info and build everything from here.
|
||||
_defaultFontInfo = DxFontInfo(desired.GetFaceName(),
|
||||
desired.GetWeight(),
|
||||
DWRITE_FONT_STYLE_NORMAL,
|
||||
DWRITE_FONT_STRETCH_NORMAL);
|
||||
|
||||
// _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;
|
||||
|
||||
bool didFallback = false;
|
||||
const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName, didFallback);
|
||||
|
||||
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;
|
||||
bool didItalicFallback = false;
|
||||
|
||||
const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName, didItalicFallback);
|
||||
|
||||
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);
|
||||
actual.SetFallback(didFallback);
|
||||
|
||||
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));
|
||||
_BuildFontRenderData(desired, actual, dpi);
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
|
@ -622,288 +442,212 @@ try
|
|||
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.
|
||||
// - Build the needed data for rendering according to the font used
|
||||
// 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.
|
||||
// - localeName - Locale to search for appropriate fonts
|
||||
// - didFallback - Indicates whether we couldn't match the user request and had to choose from a hardcoded default list.
|
||||
// - 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:
|
||||
// - 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,
|
||||
bool& didFallback) const
|
||||
// - None
|
||||
void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi)
|
||||
{
|
||||
// First attempt to find exactly what the user asked for.
|
||||
didFallback = false;
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace1> face{ nullptr };
|
||||
std::wstring fontLocaleName = UserLocaleName();
|
||||
// This is the first attempt to resolve font face after `UpdateFont`.
|
||||
// Note that the following line may cause property changes _inside_ `_defaultFontInfo` because the desired font may not exist.
|
||||
// See the implementation of `ResolveFontFaceWithFallback` for details.
|
||||
const Microsoft::WRL::ComPtr<IDWriteFontFace1> face = _defaultFontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName);
|
||||
|
||||
// GH#10211 - wrap this all up in a try/catch. If the nearby fonts are
|
||||
// corrupted, then we don't want to throw out of this top half of this
|
||||
// method. We still want to fall back to a font that's reasonable, below.
|
||||
try
|
||||
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;
|
||||
_fontSize = fontSize;
|
||||
|
||||
// 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;
|
||||
|
||||
_lineSpacing = lineSpacing;
|
||||
|
||||
// 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(_defaultFontInfo.GetFamilyName(),
|
||||
desired.GetFamily(),
|
||||
DefaultTextFormat()->GetFontWeight(),
|
||||
false,
|
||||
scaled,
|
||||
unscaled);
|
||||
|
||||
actual.SetFallback(_defaultFontInfo.GetFallback());
|
||||
|
||||
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)
|
||||
{
|
||||
face = _FindFontFace(familyName, weight, stretch, style, localeName, true);
|
||||
|
||||
if (!face)
|
||||
{
|
||||
// If we missed, try looking a little more by trimming the last word off the requested family name a few times.
|
||||
// Quite often, folks are specifying weights or something in the familyName and it causes failed resolution and
|
||||
// an unexpected error dialog. We theoretically could detect the weight words and convert them, but this
|
||||
// is the quick fix for the majority scenario.
|
||||
// The long/full fix is backlogged to GH#9744
|
||||
// Also this doesn't count as a fallback because we don't want to annoy folks with the warning dialog over
|
||||
// this resolution.
|
||||
while (!face && !familyName.empty())
|
||||
{
|
||||
const auto lastSpace = familyName.find_last_of(UNICODE_SPACE);
|
||||
|
||||
// value is unsigned and npos will be greater than size.
|
||||
// if we didn't find anything to trim, leave.
|
||||
if (lastSpace >= familyName.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// trim string down to just before the found space
|
||||
// (space found at 6... trim from 0 for 6 length will give us 0-5 as the new string)
|
||||
familyName = familyName.substr(0, lastSpace);
|
||||
|
||||
// Try to find it with the shortened family name
|
||||
face = _FindFontFace(familyName, weight, stretch, style, localeName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
// Alright, if our quick shot at trimming didn't work either...
|
||||
// move onto looking up a font from our hardcoded list of fonts
|
||||
// that should really always be available.
|
||||
if (!face)
|
||||
{
|
||||
for (const auto fallbackFace : FALLBACK_FONT_FACES)
|
||||
{
|
||||
familyName = fallbackFace;
|
||||
// With these fonts, don't attempt the nearby lookup. We're looking
|
||||
// for system fonts only. If one of the nearby fonts is causing us
|
||||
// problems (like in GH#10211), then we don't want to go anywhere
|
||||
|
||||
// near it in this part.
|
||||
face = _FindFontFace(familyName, weight, stretch, style, localeName, false);
|
||||
|
||||
if (face)
|
||||
{
|
||||
didFallback = true;
|
||||
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, false);
|
||||
|
||||
if (face)
|
||||
{
|
||||
didFallback = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth;
|
||||
}
|
||||
|
||||
THROW_HR_IF_NULL(E_FAIL, face);
|
||||
// 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;
|
||||
|
||||
return face;
|
||||
_lineMetrics = lineMetrics;
|
||||
|
||||
_glyphCell = actual.GetSize();
|
||||
}
|
||||
|
||||
// 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 bool withNearbyLookup) const
|
||||
Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::_BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName)
|
||||
{
|
||||
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 the system collection missed, try the files sitting next to our binary.
|
||||
if (withNearbyLookup && !familyExists)
|
||||
{
|
||||
auto&& nearbyCollection = NearbyCollection();
|
||||
|
||||
// May be null on OS below Windows 10. If null, just skip the attempt.
|
||||
if (nearbyCollection)
|
||||
{
|
||||
nearbyCollection.As(&fontCollection);
|
||||
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;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Digs through the directory that the current executable is running within to find
|
||||
// any TTF files sitting next to it.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - Iterable collection of filesystem paths, one per font file that was found
|
||||
[[nodiscard]] std::vector<std::filesystem::path> DxFontRenderData::s_GetNearbyFonts()
|
||||
{
|
||||
std::vector<std::filesystem::path> paths;
|
||||
|
||||
// Find the directory we're running from then enumerate all the TTF files
|
||||
// sitting next to us.
|
||||
const std::filesystem::path module{ wil::GetModuleFileNameW<std::wstring>(nullptr) };
|
||||
const auto folder{ module.parent_path() };
|
||||
|
||||
for (auto& p : std::filesystem::directory_iterator(folder))
|
||||
{
|
||||
if (p.is_regular_file())
|
||||
{
|
||||
auto extension = p.path().extension().wstring();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), std::towlower);
|
||||
|
||||
static constexpr std::wstring_view ttfExtension{ L".ttf" };
|
||||
if (ttfExtension == extension)
|
||||
{
|
||||
paths.push_back(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
Microsoft::WRL::ComPtr<IDWriteTextFormat> format;
|
||||
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontInfo.GetFamilyName().data(),
|
||||
nullptr,
|
||||
fontInfo.GetWeight(),
|
||||
fontInfo.GetStyle(),
|
||||
fontInfo.GetStretch(),
|
||||
_fontSize,
|
||||
localeName.data(),
|
||||
&format));
|
||||
return format;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "../../renderer/inc/FontInfoDesired.hpp"
|
||||
#include "DxFontInfo.h"
|
||||
#include "BoxDrawingEffect.h"
|
||||
|
||||
#include <dwrite.h>
|
||||
|
@ -31,73 +32,69 @@ namespace Microsoft::Console::Render
|
|||
DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory) noexcept;
|
||||
|
||||
// DirectWrite text analyzer from the factory
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> Analyzer() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> Analyzer();
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFallback> SystemFontFallback();
|
||||
|
||||
[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& NearbyCollection() const;
|
||||
// A locale that can be used on construction of assorted DX objects that want to know one.
|
||||
[[nodiscard]] std::wstring UserLocaleName();
|
||||
|
||||
[[nodiscard]] til::size GlyphCell() noexcept;
|
||||
[[nodiscard]] LineMetrics GetLineMetrics() noexcept;
|
||||
|
||||
// The weight of default font
|
||||
[[nodiscard]] DWRITE_FONT_WEIGHT DefaultFontWeight() noexcept;
|
||||
|
||||
// The style of default font
|
||||
[[nodiscard]] DWRITE_FONT_STYLE DefaultFontStyle() noexcept;
|
||||
|
||||
// The stretch of default font
|
||||
[[nodiscard]] DWRITE_FONT_STRETCH DefaultFontStretch() noexcept;
|
||||
|
||||
// The DirectWrite format object representing the size and other text properties to be applied (by default)
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DefaultTextFormat() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DefaultTextFormat();
|
||||
|
||||
// The DirectWrite font face to use while calculating layout (by default)
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DefaultFontFace() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DefaultFontFace();
|
||||
|
||||
// Box drawing scaling effects that are cached for the base font across layouts
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DefaultBoxDrawingEffect() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DefaultBoxDrawingEffect();
|
||||
|
||||
// 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 attributed variants of the format object representing the size and other text properties
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch);
|
||||
|
||||
// The italic variant of the font face to use while calculating layout for italic text
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> ItalicFontFace() noexcept;
|
||||
// The attributed variants of the font face to use while calculating layout
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch);
|
||||
|
||||
[[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,
|
||||
bool& didFallback) 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 bool withNearbyLookup) 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();
|
||||
|
||||
[[nodiscard]] static std::vector<std::filesystem::path> s_GetNearbyFonts();
|
||||
void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi);
|
||||
Microsoft::WRL::ComPtr<IDWriteTextFormat> _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName);
|
||||
|
||||
::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;
|
||||
|
||||
std::unordered_map<DxFontInfo, ::Microsoft::WRL::ComPtr<IDWriteTextFormat>> _textFormatMap;
|
||||
std::unordered_map<DxFontInfo, ::Microsoft::WRL::ComPtr<IDWriteFontFace1>> _fontFaceMap;
|
||||
|
||||
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontFallback> _systemFontFallback;
|
||||
mutable ::Microsoft::WRL::ComPtr<IDWriteFontCollection1> _nearbyCollection;
|
||||
std::wstring _userLocaleName;
|
||||
DxFontInfo _defaultFontInfo;
|
||||
|
||||
float _fontSize;
|
||||
til::size _glyphCell;
|
||||
|
||||
DWRITE_LINE_SPACING _lineSpacing;
|
||||
LineMetrics _lineMetrics;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<ClCompile Include="..\precomp.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\DxFontInfo.cpp" />
|
||||
<ClCompile Include="..\DxFontRenderData.cpp" />
|
||||
<ClCompile Include="..\DxRenderer.cpp" />
|
||||
</ItemGroup>
|
||||
|
@ -30,6 +31,7 @@
|
|||
<ClInclude Include="..\CustomTextRenderer.h" />
|
||||
<ClInclude Include="..\precomp.h" />
|
||||
<ClInclude Include="..\DxRenderer.hpp" />
|
||||
<ClInclude Include="..\DxFontInfo.h" />
|
||||
<ClInclude Include="..\DxFontRenderData.h" />
|
||||
<ClInclude Include="..\ScreenPixelShader.h" />
|
||||
<ClInclude Include="..\ScreenVertexShader.h" />
|
||||
|
|
|
@ -33,6 +33,7 @@ INCLUDES = \
|
|||
SOURCES = \
|
||||
$(SOURCES) \
|
||||
..\DxRenderer.cpp \
|
||||
..\DxFontInfo.cpp \
|
||||
..\DxFontRenderData.cpp \
|
||||
..\CustomTextRenderer.cpp \
|
||||
..\CustomTextLayout.cpp \
|
||||
|
|
|
@ -1818,7 +1818,7 @@ bool StateMachine::FlushToTerminal()
|
|||
// that pwchCurr was processed.
|
||||
// However, if we're here, then the processing of pwchChar triggered the
|
||||
// engine to request the entire sequence get passed through, including pwchCurr.
|
||||
success = _engine->ActionPassThroughString(_run);
|
||||
success = _engine->ActionPassThroughString(_CurrentRun());
|
||||
}
|
||||
|
||||
return success;
|
||||
|
@ -1838,17 +1838,22 @@ void StateMachine::ProcessString(const std::wstring_view string)
|
|||
size_t start = 0;
|
||||
size_t current = start;
|
||||
|
||||
_currentString = string;
|
||||
_runOffset = 0;
|
||||
_runSize = 0;
|
||||
|
||||
while (current < string.size())
|
||||
{
|
||||
// The run will be everything from the start INCLUDING the current one
|
||||
// in case we process the current character and it turns into a passthrough
|
||||
// fallback that picks up this _run inside `FlushToTerminal` above.
|
||||
_run = string.substr(start, current - start + 1);
|
||||
_runOffset = start;
|
||||
_runSize = current - start + 1;
|
||||
|
||||
if (_processingIndividually)
|
||||
{
|
||||
// If we're processing characters individually, send it to the state machine.
|
||||
ProcessCharacter(string.at(current));
|
||||
ProcessCharacter(til::at(string, current));
|
||||
++current;
|
||||
if (_state == VTStates::Ground) // Then check if we're back at ground. If we are, the next character (pwchCurr)
|
||||
{ // is the start of the next run of characters that might be printable.
|
||||
|
@ -1858,14 +1863,15 @@ void StateMachine::ProcessString(const std::wstring_view string)
|
|||
}
|
||||
else
|
||||
{
|
||||
if (_isActionableFromGround(string.at(current))) // If the current char is the start of an escape sequence, or should be executed in ground state...
|
||||
if (_isActionableFromGround(til::at(string, current))) // If the current char is the start of an escape sequence, or should be executed in ground state...
|
||||
{
|
||||
if (!_run.empty())
|
||||
if (_runSize > 0)
|
||||
{
|
||||
// Because the _run above is composed INCLUDING current, we must
|
||||
// Because the run above is composed INCLUDING current, we must
|
||||
// trim it off here since we just determined it's actionable
|
||||
// and only pass through everything before it.
|
||||
const auto allLeadingUpTo = _run.substr(0, _run.size() - 1);
|
||||
_runSize -= 1;
|
||||
const auto allLeadingUpTo = _CurrentRun();
|
||||
|
||||
_engine->ActionPrintString(allLeadingUpTo); // ... print all the chars leading up to it as part of the run...
|
||||
_trace.DispatchPrintRunTrace(allLeadingUpTo);
|
||||
|
@ -1885,14 +1891,23 @@ void StateMachine::ProcessString(const std::wstring_view string)
|
|||
// When we leave the loop, current has been advanced to the length of the string itself
|
||||
// (or one past the array index to the final char) so this `substr` operation doesn't +1
|
||||
// to include the final character (unlike the one inside the top of the loop above.)
|
||||
_run = start < string.size() ? string.substr(start) : std::wstring_view{};
|
||||
if (start < string.size())
|
||||
{
|
||||
_runOffset = start;
|
||||
_runSize = std::string::npos;
|
||||
}
|
||||
else
|
||||
{
|
||||
_runSize = 0;
|
||||
}
|
||||
|
||||
const auto run = _CurrentRun();
|
||||
// If we're at the end of the string and have remaining un-printed characters,
|
||||
if (!_processingIndividually && !_run.empty())
|
||||
if (!_processingIndividually && !run.empty())
|
||||
{
|
||||
// print the rest of the characters in the string
|
||||
_engine->ActionPrintString(_run);
|
||||
_trace.DispatchPrintRunTrace(_run);
|
||||
_engine->ActionPrintString(run);
|
||||
_trace.DispatchPrintRunTrace(run);
|
||||
}
|
||||
else if (_processingIndividually)
|
||||
{
|
||||
|
@ -1920,8 +1935,8 @@ void StateMachine::ProcessString(const std::wstring_view string)
|
|||
// Reset our state, and put all but the last char in again.
|
||||
ResetState();
|
||||
// Chars to flush are [pwchSequenceStart, pwchCurr)
|
||||
auto wchIter = _run.cbegin();
|
||||
while (wchIter < _run.cend() - 1)
|
||||
auto wchIter = run.cbegin();
|
||||
while (wchIter < run.cend() - 1)
|
||||
{
|
||||
ProcessCharacter(*wchIter);
|
||||
wchIter++;
|
||||
|
@ -1961,7 +1976,13 @@ void StateMachine::ProcessString(const std::wstring_view string)
|
|||
// If the engine doesn't require flushing at the end of the string, we
|
||||
// want to cache the partial sequence in case we have to flush the whole
|
||||
// thing to the terminal later.
|
||||
_cachedSequence = _cachedSequence.value_or(std::wstring{}) + std::wstring{ _run };
|
||||
if (!_cachedSequence)
|
||||
{
|
||||
_cachedSequence.emplace(std::wstring{});
|
||||
}
|
||||
|
||||
auto& cachedSequence = *_cachedSequence;
|
||||
cachedSequence.append(run);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,18 @@ namespace Microsoft::Console::VirtualTerminal
|
|||
|
||||
bool _isInAnsiMode;
|
||||
|
||||
std::wstring_view _run;
|
||||
std::wstring_view _currentString;
|
||||
size_t _runOffset;
|
||||
size_t _runSize;
|
||||
|
||||
// Construct current run.
|
||||
//
|
||||
// Note: We intentionally use this method to create the run lazily for better performance.
|
||||
// You may find the usage of offset & size unsafe, but under heavy load it shows noticeable performance benefit.
|
||||
std::wstring_view _CurrentRun() const
|
||||
{
|
||||
return _currentString.substr(_runOffset, _runSize);
|
||||
}
|
||||
|
||||
VTIDBuilder _identifier;
|
||||
std::vector<VTParameter> _parameters;
|
||||
|
|
|
@ -10,25 +10,20 @@ using namespace Microsoft::Console::VirtualTerminal;
|
|||
#pragma warning(disable : 26447) // The function is declared 'noexcept' but calls function '_tlgWrapBinary<wchar_t>()' which may throw exceptions
|
||||
#pragma warning(disable : 26477) // Use 'nullptr' rather than 0 or NULL
|
||||
|
||||
ParserTracing::ParserTracing() noexcept
|
||||
{
|
||||
ClearSequenceTrace();
|
||||
}
|
||||
|
||||
void ParserTracing::TraceStateChange(const std::wstring_view name) const noexcept
|
||||
void ParserTracing::TraceStateChange(_In_z_ const wchar_t* name) const noexcept
|
||||
{
|
||||
TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider,
|
||||
"StateMachine_EnterState",
|
||||
TraceLoggingCountedWideString(name.data(), gsl::narrow_cast<ULONG>(name.size())),
|
||||
TraceLoggingWideString(name),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
|
||||
}
|
||||
|
||||
void ParserTracing::TraceOnAction(const std::wstring_view name) const noexcept
|
||||
void ParserTracing::TraceOnAction(_In_z_ const wchar_t* name) const noexcept
|
||||
{
|
||||
TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider,
|
||||
"StateMachine_Action",
|
||||
TraceLoggingCountedWideString(name.data(), gsl::narrow_cast<ULONG>(name.size())),
|
||||
TraceLoggingWideString(name),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
|
||||
}
|
||||
|
@ -55,11 +50,11 @@ void ParserTracing::TraceOnExecuteFromEscape(const wchar_t wch) const noexcept
|
|||
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
|
||||
}
|
||||
|
||||
void ParserTracing::TraceOnEvent(const std::wstring_view name) const noexcept
|
||||
void ParserTracing::TraceOnEvent(_In_z_ const wchar_t* name) const noexcept
|
||||
{
|
||||
TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider,
|
||||
"StateMachine_Event",
|
||||
TraceLoggingCountedWideString(name.data(), gsl::narrow_cast<ULONG>(name.size())),
|
||||
TraceLoggingWideString(name),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
|
||||
}
|
||||
|
@ -67,12 +62,11 @@ void ParserTracing::TraceOnEvent(const std::wstring_view name) const noexcept
|
|||
void ParserTracing::TraceCharInput(const wchar_t wch)
|
||||
{
|
||||
AddSequenceTrace(wch);
|
||||
const auto sch = gsl::narrow_cast<INT16>(wch);
|
||||
|
||||
TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider,
|
||||
"StateMachine_NewChar",
|
||||
TraceLoggingWChar(wch),
|
||||
TraceLoggingHexInt16(sch),
|
||||
TraceLoggingHexInt16(gsl::narrow_cast<INT16>(wch)),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
|
||||
}
|
||||
|
@ -114,7 +108,7 @@ void ParserTracing::ClearSequenceTrace() noexcept
|
|||
}
|
||||
|
||||
// NOTE: I'm expecting this to not be null terminated
|
||||
void ParserTracing::DispatchPrintRunTrace(const std::wstring_view string) const
|
||||
void ParserTracing::DispatchPrintRunTrace(const std::wstring_view& string) const
|
||||
{
|
||||
if (string.size() == 1)
|
||||
{
|
||||
|
|
|
@ -21,19 +21,30 @@ namespace Microsoft::Console::VirtualTerminal
|
|||
class ParserTracing sealed
|
||||
{
|
||||
public:
|
||||
ParserTracing() noexcept;
|
||||
// NOTE: This code uses
|
||||
// (_In_z_ const wchar_t* name)
|
||||
// as arguments instead of the more modern std::wstring_view
|
||||
// for performance reasons.
|
||||
//
|
||||
// Passing structures larger than the register size is very expensive
|
||||
// due to Microsoft's x64 calling convention. We could reduce the
|
||||
// overhead by passing the string-view by reference, but this forces us
|
||||
// to allocate the parameters as static string-views on the data
|
||||
// segment of our binary. I've found that passing them as classic
|
||||
// C-strings is more ergonomic instead and fits the need for
|
||||
// high performance in this particular code.
|
||||
|
||||
void TraceStateChange(const std::wstring_view name) const noexcept;
|
||||
void TraceOnAction(const std::wstring_view name) const noexcept;
|
||||
void TraceStateChange(_In_z_ const wchar_t* name) const noexcept;
|
||||
void TraceOnAction(_In_z_ const wchar_t* name) const noexcept;
|
||||
void TraceOnExecute(const wchar_t wch) const noexcept;
|
||||
void TraceOnExecuteFromEscape(const wchar_t wch) const noexcept;
|
||||
void TraceOnEvent(const std::wstring_view name) const noexcept;
|
||||
void TraceOnEvent(_In_z_ const wchar_t* name) const noexcept;
|
||||
void TraceCharInput(const wchar_t wch);
|
||||
|
||||
void AddSequenceTrace(const wchar_t wch);
|
||||
void DispatchSequenceTrace(const bool fSuccess) noexcept;
|
||||
void ClearSequenceTrace() noexcept;
|
||||
void DispatchPrintRunTrace(const std::wstring_view string) const;
|
||||
void DispatchPrintRunTrace(const std::wstring_view& string) const;
|
||||
|
||||
private:
|
||||
std::wstring _sequenceTrace;
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "til/operators.h"
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
|
||||
class VisualizeControlCodesTests
|
||||
{
|
||||
TEST_CLASS(VisualizeControlCodesTests);
|
||||
|
||||
TEST_METHOD(EscapeSequence)
|
||||
{
|
||||
const std::wstring_view expected{ L"\u241b[A\u2423\u241b[B" };
|
||||
|
||||
const std::wstring_view input{ L"\u001b[A \u001b[B" };
|
||||
VERIFY_ARE_EQUAL(expected, til::visualize_control_codes(input));
|
||||
}
|
||||
};
|
46
src/til/ut_til/mutex.cpp
Normal file
46
src/til/ut_til/mutex.cpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "til/mutex.h"
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
|
||||
class MutexTests
|
||||
{
|
||||
BEGIN_TEST_CLASS(MutexTests)
|
||||
TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:10") // 10s timeout
|
||||
END_TEST_CLASS()
|
||||
|
||||
TEST_METHOD(Basic)
|
||||
{
|
||||
struct TestData
|
||||
{
|
||||
int foo;
|
||||
int bar;
|
||||
};
|
||||
|
||||
const til::shared_mutex<TestData> mutex{ TestData{ 1, 2 } };
|
||||
|
||||
{
|
||||
auto lock = mutex.lock();
|
||||
*lock = TestData{ 3, 4 };
|
||||
lock->foo = 5;
|
||||
}
|
||||
|
||||
{
|
||||
auto lock1 = mutex.lock_shared();
|
||||
auto lock2 = mutex.lock_shared();
|
||||
|
||||
VERIFY_ARE_EQUAL(5, lock1->foo);
|
||||
VERIFY_ARE_EQUAL(4, lock2->bar);
|
||||
}
|
||||
|
||||
// This is here just to ensure that the prior
|
||||
// .lock_shared() properly unlocked the mutex.
|
||||
auto lock = mutex.lock();
|
||||
}
|
||||
};
|
55
src/til/ut_til/string.cpp
Normal file
55
src/til/ut_til/string.cpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
|
||||
class StringTests
|
||||
{
|
||||
TEST_CLASS(StringTests);
|
||||
|
||||
TEST_METHOD(VisualizeControlCodes)
|
||||
{
|
||||
const std::wstring_view input{ L"\u001b[A \u001b[B\x7f" };
|
||||
const std::wstring_view expected{ L"\u241b[A\u2423\u241b[B\x2421" };
|
||||
const auto actual = til::visualize_control_codes(input);
|
||||
VERIFY_ARE_EQUAL(expected, actual);
|
||||
}
|
||||
|
||||
TEST_METHOD(StartsWith)
|
||||
{
|
||||
VERIFY_IS_TRUE(til::starts_with("", ""));
|
||||
|
||||
VERIFY_IS_TRUE(til::starts_with("abc", ""));
|
||||
VERIFY_IS_TRUE(til::starts_with("abc", "a"));
|
||||
VERIFY_IS_TRUE(til::starts_with("abc", "ab"));
|
||||
VERIFY_IS_TRUE(til::starts_with("abc", "abc"));
|
||||
VERIFY_IS_FALSE(til::starts_with("abc", "abcd"));
|
||||
|
||||
VERIFY_IS_FALSE(til::starts_with("", "abc"));
|
||||
VERIFY_IS_FALSE(til::starts_with("a", "abc"));
|
||||
VERIFY_IS_FALSE(til::starts_with("ab", "abc"));
|
||||
VERIFY_IS_TRUE(til::starts_with("abc", "abc"));
|
||||
VERIFY_IS_TRUE(til::starts_with("abcd", "abc"));
|
||||
}
|
||||
|
||||
TEST_METHOD(EndsWith)
|
||||
{
|
||||
VERIFY_IS_TRUE(til::ends_with("", ""));
|
||||
|
||||
VERIFY_IS_TRUE(til::ends_with("abc", ""));
|
||||
VERIFY_IS_TRUE(til::ends_with("abc", "c"));
|
||||
VERIFY_IS_TRUE(til::ends_with("abc", "bc"));
|
||||
VERIFY_IS_TRUE(til::ends_with("abc", "abc"));
|
||||
VERIFY_IS_FALSE(til::ends_with("abc", "0abc"));
|
||||
|
||||
VERIFY_IS_FALSE(til::ends_with("", "abc"));
|
||||
VERIFY_IS_FALSE(til::ends_with("c", "abc"));
|
||||
VERIFY_IS_FALSE(til::ends_with("bc", "abc"));
|
||||
VERIFY_IS_TRUE(til::ends_with("abc", "abc"));
|
||||
VERIFY_IS_TRUE(til::ends_with("0abc", "abc"));
|
||||
}
|
||||
};
|
42
src/til/ut_til/throttled_func.cpp
Normal file
42
src/til/ut_til/throttled_func.cpp
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "til/latch.h"
|
||||
#include "til/throttled_func.h"
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
|
||||
class ThrottledFuncTests
|
||||
{
|
||||
BEGIN_TEST_CLASS(ThrottledFuncTests)
|
||||
TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:10") // 10s timeout
|
||||
END_TEST_CLASS()
|
||||
|
||||
TEST_METHOD(Basic)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
using throttled_func = til::throttled_func_trailing<bool>;
|
||||
|
||||
til::latch latch{ 2 };
|
||||
|
||||
std::unique_ptr<throttled_func> tf;
|
||||
tf = std::make_unique<throttled_func>(10ms, [&](bool reschedule) {
|
||||
latch.count_down();
|
||||
|
||||
// This will ensure that the callback is called even if we
|
||||
// invoke the throttled_func from inside the callback itself.
|
||||
if (reschedule)
|
||||
{
|
||||
tf->operator()(false);
|
||||
}
|
||||
});
|
||||
// This will ensure that the throttled_func invokes the callback in general.
|
||||
tf->operator()(true);
|
||||
|
||||
latch.wait();
|
||||
}
|
||||
};
|
|
@ -10,24 +10,26 @@
|
|||
</PropertyGroup>
|
||||
<Import Project="$(SolutionDir)src\common.build.pre.props" />
|
||||
<ItemGroup>
|
||||
<ClCompile Include="BaseTests.cpp" />
|
||||
<ClCompile Include="BitmapTests.cpp" />
|
||||
<ClCompile Include="OperatorTests.cpp" />
|
||||
<ClCompile Include="PointTests.cpp" />
|
||||
<ClCompile Include="StaticMapTests.cpp" />
|
||||
<ClCompile Include="MathTests.cpp" />
|
||||
<ClCompile Include="RectangleTests.cpp" />
|
||||
<ClCompile Include="RunLengthEncodingTests.cpp" />
|
||||
<ClCompile Include="SizeTests.cpp" />
|
||||
<ClCompile Include="ColorTests.cpp" />
|
||||
<ClCompile Include="CoalesceTests.cpp" />
|
||||
<ClCompile Include="ReplaceTests.cpp" />
|
||||
<ClCompile Include="SomeTests.cpp" />
|
||||
<ClCompile Include="VisualizeControlCodesTests.cpp" />
|
||||
<ClCompile Include="..\precomp.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="BaseTests.cpp" />
|
||||
<ClCompile Include="BitmapTests.cpp" />
|
||||
<ClCompile Include="CoalesceTests.cpp" />
|
||||
<ClCompile Include="ColorTests.cpp" />
|
||||
<ClCompile Include="MathTests.cpp" />
|
||||
<ClCompile Include="mutex.cpp" />
|
||||
<ClCompile Include="OperatorTests.cpp" />
|
||||
<ClCompile Include="PointTests.cpp" />
|
||||
<ClCompile Include="RectangleTests.cpp" />
|
||||
<ClCompile Include="ReplaceTests.cpp" />
|
||||
<ClCompile Include="RunLengthEncodingTests.cpp" />
|
||||
<ClCompile Include="SizeTests.cpp" />
|
||||
<ClCompile Include="SomeTests.cpp" />
|
||||
<ClCompile Include="SPSCTests.cpp" />
|
||||
<ClCompile Include="StaticMapTests.cpp" />
|
||||
<ClCompile Include="string.cpp" />
|
||||
<ClCompile Include="throttled_func.cpp" />
|
||||
<ClCompile Include="u8u16convertTests.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -4,19 +4,25 @@
|
|||
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="SomeTests.cpp" />
|
||||
<ClCompile Include="..\precomp.cpp" />
|
||||
<ClCompile Include="u8u16convertTests.cpp" />
|
||||
<ClCompile Include="SizeTests.cpp" />
|
||||
<ClCompile Include="ColorTests.cpp" />
|
||||
<ClCompile Include="PointTests.cpp" />
|
||||
<ClCompile Include="StaticMapTests.cpp" />
|
||||
<ClCompile Include="RectangleTests.cpp" />
|
||||
<ClCompile Include="BitmapTests.cpp" />
|
||||
<ClCompile Include="OperatorTests.cpp" />
|
||||
<ClCompile Include="MathTests.cpp" />
|
||||
<ClCompile Include="BaseTests.cpp" />
|
||||
<ClCompile Include="BitmapTests.cpp" />
|
||||
<ClCompile Include="CoalesceTests.cpp" />
|
||||
<ClCompile Include="ColorTests.cpp" />
|
||||
<ClCompile Include="MathTests.cpp" />
|
||||
<ClCompile Include="mutex.cpp" />
|
||||
<ClCompile Include="OperatorTests.cpp" />
|
||||
<ClCompile Include="PointTests.cpp" />
|
||||
<ClCompile Include="RectangleTests.cpp" />
|
||||
<ClCompile Include="ReplaceTests.cpp" />
|
||||
<ClCompile Include="RunLengthEncodingTests.cpp" />
|
||||
<ClCompile Include="SizeTests.cpp" />
|
||||
<ClCompile Include="SomeTests.cpp" />
|
||||
<ClCompile Include="SPSCTests.cpp" />
|
||||
<ClCompile Include="StaticMapTests.cpp" />
|
||||
<ClCompile Include="string.cpp" />
|
||||
<ClCompile Include="throttled_func.cpp" />
|
||||
<ClCompile Include="u8u16convertTests.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\precomp.h" />
|
||||
|
|
Loading…
Reference in a new issue