Migrate OSS up to d3b9a780d

This commit is contained in:
Dustin Howett 2021-07-15 14:56:06 -05:00
commit cabb83db61
81 changed files with 2969 additions and 1320 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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 | |

View file

@ -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>

View file

@ -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>

View file

@ -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));
}
}

View file

@ -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*/,

View file

@ -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

View file

@ -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,

View file

@ -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:

View file

@ -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);

View file

@ -23,7 +23,6 @@
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../buffer/out/search.h"
#include "cppwinrt_utils.h"
#include "ThrottledFunc.h"
namespace ControlUnitTests
{

View file

@ -13,7 +13,6 @@
#include "../buffer/out/search.h"
#include "cppwinrt_utils.h"
#include "SearchBoxControl.h"
#include "ThrottledFunc.h"
#include "ControlInteractivity.h"

View file

@ -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>

View file

@ -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>;

View file

@ -58,3 +58,5 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalControlProvider);
#include <telemetry/ProjectTelemetry.h>
#include "til.h"
#include "ThrottledFunc.h"

View file

@ -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();
}

View file

@ -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;
};
}

View file

@ -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;
}

View file

@ -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"

View file

@ -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);

View file

@ -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">

View file

@ -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") },

View file

@ -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 } {};

View file

@ -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())

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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 };

View file

@ -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);
}
}

View file

@ -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) \

View 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()
}

View 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);
}

View 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; };
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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
//

View 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);
}
}

View 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);
}

View 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();
}

View 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;
};
}

View 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);
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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();

View file

@ -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;

View file

@ -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);

View file

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

View file

@ -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();

View file

@ -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>

View file

@ -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>

View 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>;

View file

@ -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

View file

@ -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);

View file

@ -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.

View file

@ -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;
};
}

View file

@ -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
View 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), &current, 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
View 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

View file

@ -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);
};
}

View 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

View file

@ -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

View 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;
}

View 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 });
}
};
}

View file

@ -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;
}

View file

@ -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;
};
}

View file

@ -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" />

View file

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

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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)
{

View file

@ -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;

View file

@ -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
View 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
View 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"));
}
};

View 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();
}
};

View file

@ -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>

View file

@ -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" />