diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 3452bf4a8..fcaaf797a 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -12,6 +12,7 @@ downsides dze dzhe Enum'd +formattings ftp geeksforgeeks ghe @@ -34,6 +35,7 @@ overlined postmodern ptys qof +qps reimplementation reserialization reserialize @@ -46,6 +48,7 @@ tokenizes tonos tshe UIs +und versioned We'd wildcards diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 8252b10b7..032570e55 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -4,6 +4,8 @@ alignas alignof bitfield bitfields +BUILDBRANCH +BUILDMSG BUILDNUMBER charconv CLASSNOTAVAILABLE @@ -45,6 +47,7 @@ IBind IBox IClass IComparable +IComparer IConnection ICustom IDialog @@ -71,6 +74,7 @@ llu localtime lround LSHIFT +mov msappx MULTIPLEUSE NCHITTEST @@ -85,6 +89,8 @@ NOREPEAT ntprivapi oaidl ocidl +ODR +offsetof osver OSVERSIONINFOEXW otms @@ -93,6 +99,7 @@ overridable PAGESCROLL PICKFOLDERS pmr +rcx REGCLS RETURNCMD rfind @@ -124,6 +131,7 @@ tmp tolower TTask TVal +UChar UPDATEINIFILE userenv wcsstr @@ -132,6 +140,7 @@ winmain wpc wsregex wwinmain +xchg XDocument XElement xfacet @@ -146,6 +155,7 @@ xlocnum xloctime xmemory XParse +xpath xstddef xstring xtree diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index b8187ab50..e6572d671 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -29,9 +29,11 @@ mfcribbon microsoft microsoftonline msixbundle +MSVC muxc netcore osgvsowi +PFILETIME pgc pgo pgosweep @@ -39,10 +41,12 @@ powerrename powershell propkey pscustomobject +QWORD robocopy SACLs Shobjidl Skype +SRW sxs Sysinternals sysnative diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 722631b26..6ddbe8c3f 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -163,6 +163,7 @@ BPBF bpp BPPF branchconfig +brandings BRK Browsable bsearch @@ -1941,6 +1942,7 @@ REGSTR reingest Relayout RELBINPATH +remoting Remoting renamer renderengine diff --git a/.github/actions/spelling/patterns/patterns.txt b/.github/actions/spelling/patterns/patterns.txt index bee4446d4..882243396 100644 --- a/.github/actions/spelling/patterns/patterns.txt +++ b/.github/actions/spelling/patterns/patterns.txt @@ -23,3 +23,4 @@ VERIFY_ARE_EQUAL\(L"[^"]+" "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\+/" std::memory_order_[\w]+ D2DERR_SHADER_COMPILE_FAILED +TIL_FEATURE_[0-9A-Z_]+ diff --git a/.vscode/settings.json b/.vscode/settings.json index 2b746e3d1..142fa20f6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -100,6 +100,6 @@ "**/bin/**": true, "**/obj/**": true, "**/packages/**": true, - "**/generated files/**": true + "**/Generated Files/**": true } } \ No newline at end of file diff --git a/Scratch.sln b/Scratch.sln new file mode 100644 index 000000000..ac6caf341 --- /dev/null +++ b/Scratch.sln @@ -0,0 +1,221 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31205.134 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Package", "scratch\ScratchIslandApp\Package\Package.wapproj", "{CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleAppLib", "scratch\ScratchIslandApp\SampleApp\SampleAppLib.vcxproj", "{A4394404-37F7-41C1-802B-49788D3720E3}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleApp", "scratch\ScratchIslandApp\SampleApp\dll\SampleApp.vcxproj", "{26C51792-41A3-4FE0-AB5E-8B69D557BF91}" + ProjectSection(ProjectDependencies) = postProject + {A4394404-37F7-41C1-802B-49788D3720E3} = {A4394404-37F7-41C1-802B-49788D3720E3} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WindowExe", "scratch\ScratchIslandApp\WindowExe\WindowExe.vcxproj", "{B4427499-9FDE-4208-B456-5BC580637633}" + ProjectSection(ProjectDependencies) = postProject + {26C51792-41A3-4FE0-AB5E-8B69D557BF91} = {26C51792-41A3-4FE0-AB5E-8B69D557BF91} + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common Props", "Common Props", "{53DD5520-E64C-4C06-B472-7CE62CA539C9}" + ProjectSection(SolutionItems) = preProject + src\common.build.post.props = src\common.build.post.props + src\common.build.pre.props = src\common.build.pre.props + src\common.build.tests.props = src\common.build.tests.props + common.openconsole.props = common.openconsole.props + src\cppwinrt.build.post.props = src\cppwinrt.build.post.props + src\cppwinrt.build.pre.props = src\cppwinrt.build.pre.props + src\wap-common.build.post.props = src\wap-common.build.post.props + src\wap-common.build.pre.props = src\wap-common.build.pre.props + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fmt", "src\dep\fmt\fmt.vcxproj", "{6BAE5851-50D5-4934-8D5E-30361A8A40F3}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Types", "src\types\lib\types.vcxproj", "{18D09A24-8240-42D6-8CB6-236EEE820263}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dependencies", "dependencies", "{75AC9360-76FD-4ABC-AFEC-EF342BD2B3E9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + AuditMode|ARM64 = AuditMode|ARM64 + AuditMode|x64 = AuditMode|x64 + AuditMode|x86 = AuditMode|x86 + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Fuzzing|ARM64 = Fuzzing|ARM64 + Fuzzing|x64 = Fuzzing|x64 + Fuzzing|x86 = Fuzzing|x86 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|ARM64.ActiveCfg = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|ARM64.Build.0 = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|ARM64.Deploy.0 = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|x64.ActiveCfg = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|x64.Build.0 = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|x64.Deploy.0 = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|x86.ActiveCfg = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|x86.Build.0 = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.AuditMode|x86.Deploy.0 = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|ARM64.Build.0 = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|x64.ActiveCfg = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|x64.Build.0 = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|x64.Deploy.0 = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|x86.ActiveCfg = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|x86.Build.0 = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Debug|x86.Deploy.0 = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|ARM64.Build.0 = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|ARM64.Deploy.0 = Debug|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|x64.ActiveCfg = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|x64.Build.0 = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|x64.Deploy.0 = Debug|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|x86.ActiveCfg = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|x86.Build.0 = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Fuzzing|x86.Deploy.0 = Debug|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|ARM64.ActiveCfg = Release|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|ARM64.Build.0 = Release|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|ARM64.Deploy.0 = Release|ARM64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|x64.ActiveCfg = Release|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|x64.Build.0 = Release|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|x64.Deploy.0 = Release|x64 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|x86.ActiveCfg = Release|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|x86.Build.0 = Release|x86 + {CF31505E-3BAE-4C0A-81D7-F1EB279F40BB}.Release|x86.Deploy.0 = Release|x86 + {A4394404-37F7-41C1-802B-49788D3720E3}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.AuditMode|x64.Build.0 = AuditMode|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {A4394404-37F7-41C1-802B-49788D3720E3}.AuditMode|x86.Build.0 = AuditMode|Win32 + {A4394404-37F7-41C1-802B-49788D3720E3}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Debug|ARM64.Build.0 = Debug|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Debug|x64.ActiveCfg = Debug|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Debug|x64.Build.0 = Debug|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Debug|x86.ActiveCfg = Debug|Win32 + {A4394404-37F7-41C1-802B-49788D3720E3}.Debug|x86.Build.0 = Debug|Win32 + {A4394404-37F7-41C1-802B-49788D3720E3}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Fuzzing|x64.Build.0 = Fuzzing|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {A4394404-37F7-41C1-802B-49788D3720E3}.Fuzzing|x86.Build.0 = Fuzzing|Win32 + {A4394404-37F7-41C1-802B-49788D3720E3}.Release|ARM64.ActiveCfg = Release|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Release|ARM64.Build.0 = Release|ARM64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Release|x64.ActiveCfg = Release|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Release|x64.Build.0 = Release|x64 + {A4394404-37F7-41C1-802B-49788D3720E3}.Release|x86.ActiveCfg = Release|Win32 + {A4394404-37F7-41C1-802B-49788D3720E3}.Release|x86.Build.0 = Release|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.AuditMode|x64.Build.0 = AuditMode|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.AuditMode|x86.Build.0 = AuditMode|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Debug|ARM64.Build.0 = Debug|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Debug|x64.ActiveCfg = Debug|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Debug|x64.Build.0 = Debug|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Debug|x86.ActiveCfg = Debug|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Debug|x86.Build.0 = Debug|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Fuzzing|x64.Build.0 = Fuzzing|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Fuzzing|x86.Build.0 = Fuzzing|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Release|ARM64.ActiveCfg = Release|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Release|ARM64.Build.0 = Release|ARM64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Release|x64.ActiveCfg = Release|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Release|x64.Build.0 = Release|x64 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Release|x86.ActiveCfg = Release|Win32 + {26C51792-41A3-4FE0-AB5E-8B69D557BF91}.Release|x86.Build.0 = Release|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.AuditMode|x64.Build.0 = AuditMode|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.AuditMode|x86.Build.0 = AuditMode|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.Debug|ARM64.Build.0 = Debug|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.Debug|x64.ActiveCfg = Debug|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.Debug|x64.Build.0 = Debug|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.Debug|x86.ActiveCfg = Debug|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.Debug|x86.Build.0 = Debug|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.Fuzzing|x64.Build.0 = Fuzzing|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.Fuzzing|x86.Build.0 = Fuzzing|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.Release|ARM64.ActiveCfg = Release|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.Release|ARM64.Build.0 = Release|ARM64 + {B4427499-9FDE-4208-B456-5BC580637633}.Release|x64.ActiveCfg = Release|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.Release|x64.Build.0 = Release|x64 + {B4427499-9FDE-4208-B456-5BC580637633}.Release|x86.ActiveCfg = Release|Win32 + {B4427499-9FDE-4208-B456-5BC580637633}.Release|x86.Build.0 = Release|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|x64.Build.0 = AuditMode|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|x86.Build.0 = AuditMode|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|ARM64.Build.0 = Debug|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|x64.ActiveCfg = Debug|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|x64.Build.0 = Debug|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|x86.ActiveCfg = Debug|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|x86.Build.0 = Debug|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|x64.Build.0 = Fuzzing|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|x86.Build.0 = Fuzzing|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|ARM64.ActiveCfg = Release|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|ARM64.Build.0 = Release|ARM64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|x64.ActiveCfg = Release|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|x64.Build.0 = Release|x64 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|x86.ActiveCfg = Release|Win32 + {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|x86.Build.0 = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x64.Build.0 = AuditMode|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x86.Build.0 = AuditMode|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|ARM64.Build.0 = Debug|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x64.ActiveCfg = Debug|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x64.Build.0 = Debug|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x86.ActiveCfg = Debug|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x86.Build.0 = Debug|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|x64.Build.0 = Fuzzing|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|x86.Build.0 = Fuzzing|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|ARM64.ActiveCfg = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|ARM64.Build.0 = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x64.ActiveCfg = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x64.Build.0 = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x86.ActiveCfg = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6BAE5851-50D5-4934-8D5E-30361A8A40F3} = {75AC9360-76FD-4ABC-AFEC-EF342BD2B3E9} + {18D09A24-8240-42D6-8CB6-236EEE820263} = {75AC9360-76FD-4ABC-AFEC-EF342BD2B3E9} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {05EAE315-9188-4D7B-B889-7D5F480A8915} + EndGlobalSection +EndGlobal diff --git a/build/pipelines/templates/build-console-ci.yml b/build/pipelines/templates/build-console-ci.yml index 75ef642c1..0b9ce8685 100644 --- a/build/pipelines/templates/build-console-ci.yml +++ b/build/pipelines/templates/build-console-ci.yml @@ -32,6 +32,8 @@ jobs: dependsOn: Build${{ parameters.platform }}${{ parameters.configuration }} condition: and(succeeded(), and(eq('${{ parameters.platform }}', 'x64'), not(eq(variables['Build.Reason'], 'PullRequest')))) testSuite: 'DevTestSuite' + platform: ${{ parameters.platform }} + configuration: ${{ parameters.configuration }} rerunPassesRequiredToAvoidFailure: ${{ parameters.rerunPassesRequiredToAvoidFailure }} - template: helix-processtestresults-job.yml diff --git a/build/rules/GenerateFeatureFlags.proj b/build/rules/GenerateFeatureFlags.proj new file mode 100644 index 000000000..93c50a993 --- /dev/null +++ b/build/rules/GenerateFeatureFlags.proj @@ -0,0 +1,97 @@ + + + + + + + + Release + AnyCPU + + + Fuzzing + AnyCPU + + + AuditMode + AnyCPU + + + Debug + AnyCPU + + + + + d97c3c61-53cd-4e72-919b-9a0940e038f9 + + + + $(SolutionDir)obj\$(Configuration)\GenerateFeatureFlags\ + $(SolutionDir)bin\$(Configuration)\ + + <_WTBrandingName Condition="'$(WindowsTerminalBranding)'=='Preview'">Preview + <_WTBrandingName Condition="'$(WindowsTerminalBranding)'=='Release'">Release + <_WTBrandingName Condition="'$(_WTBrandingName)'==''">Dev + + + + + + + + + <_BrandingLines Include="$(_WTBrandingName)" /> + + + + + + + <_BranchBrandingCacheFiles Include="$(IntermediateOutputPath)branch_branding_cache.txt" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/consolegit2gitfilters.json b/consolegit2gitfilters.json index 5a760692b..d7781402d 100644 --- a/consolegit2gitfilters.json +++ b/consolegit2gitfilters.json @@ -19,10 +19,13 @@ "/.github/", "/samples/", "/res/terminal/", + "/res/fonts/", "/doc/specs/", "/doc/cascadia/", "/doc/user-docs/", "/src/tools/MonarchPeasantSample/", + "/scratch/", + "Scratch.sln", ], "SuffixFilters": [ ".dbb", diff --git a/dep/wil b/dep/wil index 3c00e7f1d..2e225973d 160000 --- a/dep/wil +++ b/dep/wil @@ -1 +1 @@ -Subproject commit 3c00e7f1d8cf9930bbb8e5be3ef0df65c84e8928 +Subproject commit 2e225973d6c2ecf17fb4d376ddbeedb6db7dd82f diff --git a/doc/TAEF.md b/doc/TAEF.md index 055158427..02edbc825 100644 --- a/doc/TAEF.md +++ b/doc/TAEF.md @@ -16,7 +16,7 @@ If you have Visual Studio and related C++ components installed, and you have suc > Note that you cannot easily run TAEF tests directly through Visual Studio. The `Microsoft.Taef` NuGet package comes with an adapter that will let you browse and execute TAEF tests inside of Visual Studio, but its performance and reliability prevent us from recommending it here. -In a "normal" CMD environment, `te.exe` may not be directly available. Try the following command to set up the development enviroment first: +In a "normal" CMD environment, `te.exe` may not be directly available. Try the following command to set up the development environment first: ```shell .\tools\razzle.cmd diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index c5cb67540..ac27a5b19 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -191,6 +191,7 @@ "find", "findMatch", "focusPane", + "globalSummon", "identifyWindow", "identifyWindows", "moveFocus", @@ -206,6 +207,7 @@ "prevTab", "renameTab", "openTabRenamer", + "quakeMode", "resetFontSize", "resizePane", "renameWindow", @@ -544,12 +546,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" + ] } } @@ -817,6 +821,63 @@ } ] }, + "GlobalSummonAction": { + "description": "This is a special action that works globally in the OS, rather than only in the context of the terminal window. When pressed, this action will summon the terminal window.", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "globalSummon" }, + "desktop": { + "type": "string", + "default": "toCurrent", + "description": "This controls how the terminal should interact with virtual desktops.\n- \"any\": Leave the window on whichever desktop it's already on - will switch to that desktop as the window is activated.\n- \"toCurrent\" (default): Move the window to the current virtual desktop.\n- \"onCurrent\": Only summon the window if it's already on the current virtual desktop. ", + "enum": [ + "any", + "toCurrent", + "onCurrent" + ] + }, + "monitor": { + "type": "string", + "default": "toMouse", + "description": "This controls the monitor that the window will be summoned from/to.\n- \"any\": Summon the most recently used window, regardless of which monitor it's currently on.\n- \"toCurrent\": Summon the most recently used window to the monitor with the current foreground window.\n- \"toMouse\" (default): Summon the most recently used window to the monitor where the mouse cursor is.", + "enum": [ + "any", + "toCurrent", + "toMouse" + ] + }, + "name": { + "type": "string", + "description": "When provided, summon the window whose name or ID matches the given name value. If no such window exists, then create a new window with that name." + }, + "dropdownDuration": { + "type": "number", + "minimum": 0, + "default": 0, + "description": "When provided with a positive number, \"slide\" the window in from the top of the screen using an animation that lasts dropdownDuration milliseconds." + }, + "toggleVisibility": { + "type": "boolean", + "default": true, + "description": "When true, pressing the assigned keys for this action will dismiss (minimize) the window when the window is currently the foreground window." + } + } + } + ] + }, + "QuakeModeAction": { + "description": "This action is a special variation of the globalSummon action. It specifically summons a window called \"_quake\". If you would like to change the behavior of the quakeMode action, we recommended creating a new globalSummon entry.", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "quakeMode" } + } + } + ] + }, "Keybinding": { "additionalProperties": false, "properties": { @@ -848,6 +909,8 @@ { "$ref": "#/definitions/RenameTabAction" }, { "$ref": "#/definitions/RenameWindowAction" }, { "$ref": "#/definitions/FocusPaneAction" }, + { "$ref": "#/definitions/GlobalSummonAction" }, + { "$ref": "#/definitions/QuakeModeAction" }, { "type": "null" } ] }, @@ -868,16 +931,38 @@ }, "icon": { "$ref": "#/definitions/Icon" }, "name": { - "description": "The name that will appear in the command palette. If one isn't provided, the terminal will attempt to automatically generate a name.", + "description": "The name that will appear in the command palette. If one isn't provided, the terminal will attempt to automatically generate a name.\nIf name is a string, it will be the name of the command.\nIf name is a object, the key property of the object will be used to lookup a localized string resource for the command", + "properties": { + "key": { + "type": "string" + } + }, "type": [ "string", + "object", "null" ] + }, + "iterateOn": { + "type": "string", + "description": "Used to create iterable commands based on other objects in your settings. Possible values:\n- \"profiles\" \n- \"schemes\"", + "enum": [ + "profiles", + "schemes" + ] + }, + "commands": { + "description": "List of commands to execute", + "items": { + "$ref": "#/definitions/Keybinding/properties/command" + }, + "minItems": 1, + "type": "array" } }, - "required": [ - "command", - "keys" + "anyOf": [ + {"required": ["name","commands"]}, + {"required": ["command"]} ], "type": "object" }, @@ -1011,13 +1096,26 @@ "type": [ "integer", "string" ], "deprecated": true }, + "actions": { + "description": "Properties are specific to each custom action.", + "items": { + "$ref": "#/definitions/Keybinding" + }, + "type": "array" + }, "keybindings": { - "description": "Properties are specific to each custom key binding.", + "description": "[deprecated] Use actions instead.", + "deprecated": true, "items": { "$ref": "#/definitions/Keybinding" }, "type": "array" }, + "language": { + "default": "", + "description": "Sets an override for the app's preferred language, expressed as a BCP-47 language tag like en-US.", + "type": "string" + }, "theme": { "default": "system", "description": "Sets the theme of the application. The special value \"system\" refers to the active Windows system theme.", diff --git a/doc/feature_flags.md b/doc/feature_flags.md new file mode 100644 index 000000000..f4c2a7d68 --- /dev/null +++ b/doc/feature_flags.md @@ -0,0 +1,65 @@ +# til::feature + +Feature flags are controlled by an XML document stored at `src/features.xml`. + +## Example Document + +```xml + + + + + Feature_XYZ + + Does a cool thing + + + 1234 + + + AlwaysEnabled|AlwaysDisabled + + + + branch/with/wildcard/* + + + + + + ... + + + + + + Release + + + + + + ... + + + + + + +``` + +## Notes + +Features that are disabled for Release using `alwaysDisabledReleaseTokens` are +*always* disabled in Release, even if they come from a branch that would have +been enabled by the wildcard. + +### Precedence + +1. `alwaysDisabledReleaseTokens` +2. Enabled branches +3. Disabled branches + * The longest branch token that matches your branch will win. +3. Enabled brandings +4. Disabled brandings +5. The feature's default state diff --git a/res/Cascadia.ttf b/res/Cascadia.ttf deleted file mode 100644 index 3c232862f..000000000 Binary files a/res/Cascadia.ttf and /dev/null differ diff --git a/res/CascadiaMono.ttf b/res/CascadiaMono.ttf deleted file mode 100644 index 253eae2d1..000000000 Binary files a/res/CascadiaMono.ttf and /dev/null differ diff --git a/res/README.md b/res/README.md index 0fb16a9ff..b70cd4da8 100644 --- a/res/README.md +++ b/res/README.md @@ -6,16 +6,3 @@ The images in this directory do not fall under the same [license](https://raw.gi of the Windows Terminal code. Please consult the [license](./LICENSE) in this directory for terms applicable to the image assets in this directory. - -## Fonts - -The fonts in this directory do not fall under the same [license](https://raw.githubusercontent.com/microsoft/terminal/main/LICENSE) as the rest -of the Windows Terminal code. - -Please consult the [license](https://raw.githubusercontent.com/microsoft/cascadia-code/main/LICENSE) in the -[microsoft/cascadia-code](https://github.com/microsoft/cascadia-code) repository for terms applicable to the fonts in this directory. - -### Fonts Included - -* Cascadia Code, Cascadia Mono (2102.25) - * from microsoft/cascadia-code@911dc421f333e3b72b97381d16fee5b71eb48f04 diff --git a/res/fonts/CascadiaCode.ttf b/res/fonts/CascadiaCode.ttf new file mode 100644 index 000000000..117b8de3e Binary files /dev/null and b/res/fonts/CascadiaCode.ttf differ diff --git a/res/fonts/CascadiaCodeItalic.ttf b/res/fonts/CascadiaCodeItalic.ttf new file mode 100644 index 000000000..0e2587de4 Binary files /dev/null and b/res/fonts/CascadiaCodeItalic.ttf differ diff --git a/res/fonts/CascadiaMono.ttf b/res/fonts/CascadiaMono.ttf new file mode 100644 index 000000000..6a66e2b0e Binary files /dev/null and b/res/fonts/CascadiaMono.ttf differ diff --git a/res/fonts/CascadiaMonoItalic.ttf b/res/fonts/CascadiaMonoItalic.ttf new file mode 100644 index 000000000..086b1b161 Binary files /dev/null and b/res/fonts/CascadiaMonoItalic.ttf differ diff --git a/res/fonts/README.md b/res/fonts/README.md new file mode 100644 index 000000000..d4ece8657 --- /dev/null +++ b/res/fonts/README.md @@ -0,0 +1,12 @@ +# Windows Terminal and Console Assets (Fonts) + +The fonts in this directory do not fall under the same [license](https://raw.githubusercontent.com/microsoft/terminal/main/LICENSE) as the rest +of the Windows Terminal code. + +Please consult the [license](https://raw.githubusercontent.com/microsoft/cascadia-code/main/LICENSE) in the +[microsoft/cascadia-code](https://github.com/microsoft/cascadia-code) repository for terms applicable to the fonts in this directory. + +### Fonts Included + +* Cascadia Code, Cascadia Mono (2106.17) + * from microsoft/cascadia-code@fb0bce69c1c12f6c298b8bc1c1d181868f5daa9a diff --git a/scratch/ScratchIslandApp/Package/Package.appxmanifest b/scratch/ScratchIslandApp/Package/Package.appxmanifest new file mode 100644 index 000000000..dcd54adca --- /dev/null +++ b/scratch/ScratchIslandApp/Package/Package.appxmanifest @@ -0,0 +1,68 @@ + + + + + + + + Sample App + A Lone Developer + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scratch/ScratchIslandApp/Package/Package.wapproj b/scratch/ScratchIslandApp/Package/Package.wapproj new file mode 100644 index 000000000..262719541 --- /dev/null +++ b/scratch/ScratchIslandApp/Package/Package.wapproj @@ -0,0 +1,155 @@ + + + + + + + false + false + + + cf31505e-3bae-4c0a-81d7-f1eb279f40bb + ..\WindowExe\WindowExe.vcxproj + NativeOnly + + + false + Never + + + true + False + Package_TemporaryKey.pfx + + + + + + + Designer + + + + + + + + + + + + + + + + + + $(OpenConsoleCommonOutDir)TerminalConnection\Microsoft.Terminal.TerminalConnection.winmd + $(OpenConsoleCommonOutDir)TerminalConnection\TerminalConnection.dll + true + true + true + + + $(OpenConsoleCommonOutDir)Microsoft.Terminal.Control\Microsoft.Terminal.Control.winmd + $(OpenConsoleCommonOutDir)Microsoft.Terminal.Control\Microsoft.Terminal.Control.dll + true + true + true + + + + + + + + <_TemporaryFilteredWapProjOutput Include="@(_FilteredNonWapProjProjectOutput)" /> + <_FilteredNonWapProjProjectOutput Remove="@(_TemporaryFilteredWapProjOutput)" /> + <_FilteredNonWapProjProjectOutput Include="@(_TemporaryFilteredWapProjOutput)"> + + + + + + + + + + <_GenerateProjectPriFileDependsOn Condition="$(MSBuildVersion) < '16.3.0'">OpenConsoleLiftDesktopBridgePriFiles;$(_GenerateProjectPriFileDependsOn) + + + + <_PriFile Include="@(_NonWapProjProjectOutput)" Condition="'%(Extension)' == '.pri'" /> + + + + + + + + $([MSBuild]::Unescape('$(WapProjBeforeGenerateAppxManifestDependsOn.Replace('_RemoveAllNonWapUWPItems', '_OpenConsoleRemoveAllNonWapUWPItems'))')) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + diff --git a/scratch/ScratchIslandApp/Package/Resources/Resources.resw b/scratch/ScratchIslandApp/Package/Resources/Resources.resw new file mode 100644 index 000000000..872f430e7 --- /dev/null +++ b/scratch/ScratchIslandApp/Package/Resources/Resources.resw @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Scratch XAML Island App + + + Scratch App + + diff --git a/scratch/ScratchIslandApp/Package/Resources/en-US/Resources.resw b/scratch/ScratchIslandApp/Package/Resources/en-US/Resources.resw new file mode 100644 index 000000000..4ffee428e --- /dev/null +++ b/scratch/ScratchIslandApp/Package/Resources/en-US/Resources.resw @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + A scratch app for XAML Islands tests + + diff --git a/scratch/ScratchIslandApp/SampleApp/App.base.h b/scratch/ScratchIslandApp/SampleApp/App.base.h new file mode 100644 index 000000000..489926507 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/App.base.h @@ -0,0 +1,32 @@ +#pragma once + +namespace winrt::SampleApp::implementation +{ + template + struct App_baseWithProvider : public App_base + { + using IXamlType = ::winrt::Windows::UI::Xaml::Markup::IXamlType; + + IXamlType GetXamlType(::winrt::Windows::UI::Xaml::Interop::TypeName const& type) + { + return _appProvider.GetXamlType(type); + } + + IXamlType GetXamlType(::winrt::hstring const& fullName) + { + return _appProvider.GetXamlType(fullName); + } + + ::winrt::com_array<::winrt::Windows::UI::Xaml::Markup::XmlnsDefinition> GetXmlnsDefinitions() + { + return _appProvider.GetXmlnsDefinitions(); + } + + private: + bool _contentLoaded{ false }; + winrt::SampleApp::XamlMetaDataProvider _appProvider; + }; + + template + using AppT2 = App_baseWithProvider; +} diff --git a/scratch/ScratchIslandApp/SampleApp/App.cpp b/scratch/ScratchIslandApp/SampleApp/App.cpp new file mode 100644 index 000000000..5a2ae523f --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/App.cpp @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "App.h" +#include "App.g.cpp" + +using namespace winrt; +using namespace winrt::Windows::ApplicationModel::Activation; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Xaml::Navigation; + +namespace winrt::SampleApp::implementation +{ + App::App() + { + // This is the same trick that Initialize() is about to use to figure out whether we're coming + // from a UWP context or from a Win32 context + // See https://github.com/windows-toolkit/Microsoft.Toolkit.Win32/blob/52611c57d89554f357f281d0c79036426a7d9257/Microsoft.Toolkit.Win32.UI.XamlApplication/XamlApplication.cpp#L42 + const auto dispatcherQueue = ::winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + if (dispatcherQueue) + { + _isUwp = true; + } + + Initialize(); + + // Disable XAML's automatic backplating of text when in High Contrast + // mode: we want full control of and responsibility for the foreground + // and background colors that we draw in XAML. + HighContrastAdjustment(::winrt::Windows::UI::Xaml::ApplicationHighContrastAdjustment::None); + } + + SampleAppLogic App::Logic() + { + static SampleAppLogic logic; + return logic; + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + void App::OnLaunched(LaunchActivatedEventArgs const& /*e*/) + { + // if this is a UWP... it means its our problem to hook up the content to the window here. + if (_isUwp) + { + auto content = Window::Current().Content(); + if (content == nullptr) + { + auto logic = Logic(); + logic.Create(); + + auto page = logic.GetRoot().as(); + + Window::Current().Content(page); + Window::Current().Activate(); + } + } + } +} diff --git a/scratch/ScratchIslandApp/SampleApp/App.h b/scratch/ScratchIslandApp/SampleApp/App.h new file mode 100644 index 000000000..fb6235d75 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/App.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "App.g.h" +#include "App.base.h" + +namespace winrt::SampleApp::implementation +{ + struct App : AppT2 + { + public: + App(); + void OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs const&); + + SampleApp::SampleAppLogic Logic(); + + private: + bool _isUwp = false; + }; +} + +namespace winrt::SampleApp::factory_implementation +{ + struct App : AppT + { + }; +} diff --git a/scratch/ScratchIslandApp/SampleApp/App.idl b/scratch/ScratchIslandApp/SampleApp/App.idl new file mode 100644 index 000000000..e51a73332 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/App.idl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "SampleAppLogic.idl"; + +namespace SampleApp +{ + // ADD ARBITRARY APP LOGIC TO SampleAppLogic.idl, NOT HERE. + // This is for XAML platform setup only. + [default_interface] runtimeclass App : Microsoft.Toolkit.Win32.UI.XamlHost.XamlApplication + { + App(); + + SampleAppLogic Logic { get; }; + } +} diff --git a/scratch/ScratchIslandApp/SampleApp/App.xaml b/scratch/ScratchIslandApp/SampleApp/App.xaml new file mode 100644 index 000000000..c6a2328f4 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/App.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + 8,0,8,0 + + + + + + + + + + + + + + + + + + + + + diff --git a/scratch/ScratchIslandApp/SampleApp/MyPage.cpp b/scratch/ScratchIslandApp/SampleApp/MyPage.cpp new file mode 100644 index 000000000..d4af3d80e --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/MyPage.cpp @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "MyPage.h" +#include +#include "MyPage.g.cpp" +#include "..\..\..\src\cascadia\UnitTests_Control\MockControlSettings.h" + +using namespace std::chrono_literals; +using namespace winrt::Microsoft::Terminal; + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +namespace winrt::SampleApp::implementation +{ + MyPage::MyPage() + { + InitializeComponent(); + } + + void MyPage::Create() + { + TerminalConnection::EchoConnection conn{}; + auto settings = winrt::make_self(); + + Control::TermControl control{ *settings, conn }; + + InProcContent().Children().Append(control); + + // Once the control loads (and not before that), write some text for debugging: + control.Initialized([conn](auto&&, auto&&) { + conn.WriteInput(L"This TermControl is hosted in-proc..."); + }); + } + + // Method Description: + // - Gets the title of the currently focused terminal control. If there + // isn't a control selected for any reason, returns "Windows Terminal" + // Arguments: + // - + // Return Value: + // - the title of the focused control if there is one, else "Windows Terminal" + hstring MyPage::Title() + { + return { L"Sample Application" }; + } + +} diff --git a/scratch/ScratchIslandApp/SampleApp/MyPage.h b/scratch/ScratchIslandApp/SampleApp/MyPage.h new file mode 100644 index 000000000..c16c02bb3 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/MyPage.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "MyPage.g.h" +#include "../../../src/cascadia/inc/cppwinrt_utils.h" + +namespace winrt::SampleApp::implementation +{ + struct MyPage : MyPageT + { + public: + MyPage(); + + void Create(); + + hstring Title(); + + private: + friend struct MyPageT; // for Xaml to bind events + }; +} + +namespace winrt::SampleApp::factory_implementation +{ + BASIC_FACTORY(MyPage); +} diff --git a/scratch/ScratchIslandApp/SampleApp/MyPage.idl b/scratch/ScratchIslandApp/SampleApp/MyPage.idl new file mode 100644 index 000000000..d3d0645b5 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/MyPage.idl @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace SampleApp +{ + [default_interface] runtimeclass MyPage : Windows.UI.Xaml.Controls.Page + { + MyPage(); + } +} diff --git a/scratch/ScratchIslandApp/SampleApp/MyPage.xaml b/scratch/ScratchIslandApp/SampleApp/MyPage.xaml new file mode 100644 index 000000000..f6e129ee7 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/MyPage.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scratch/ScratchIslandApp/SampleApp/MySettings.cpp b/scratch/ScratchIslandApp/SampleApp/MySettings.cpp new file mode 100644 index 000000000..c042a34e7 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/MySettings.cpp @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "MySettings.h" +#include "MySettings.g.cpp" + +namespace winrt::SampleApp::implementation +{ +} diff --git a/scratch/ScratchIslandApp/SampleApp/MySettings.h b/scratch/ScratchIslandApp/SampleApp/MySettings.h new file mode 100644 index 000000000..170c9b540 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/MySettings.h @@ -0,0 +1,97 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. +--*/ +#pragma once +#include "../../inc/cppwinrt_utils.h" +#include +#include +#include "MySettings.g.h" + +namespace winrt::SampleApp::implementation +{ + struct MySettings : MySettingsT + { + public: + MySettings() = default; + + // --------------------------- Core Settings --------------------------- + // All of these settings are defined in ICoreSettings. + + WINRT_PROPERTY(til::color, DefaultForeground, DEFAULT_FOREGROUND); + WINRT_PROPERTY(til::color, DefaultBackground, DEFAULT_BACKGROUND); + WINRT_PROPERTY(til::color, SelectionBackground, DEFAULT_FOREGROUND); + WINRT_PROPERTY(int32_t, HistorySize, DEFAULT_HISTORY_SIZE); + WINRT_PROPERTY(int32_t, InitialRows, 30); + WINRT_PROPERTY(int32_t, InitialCols, 80); + + WINRT_PROPERTY(bool, SnapOnInput, true); + WINRT_PROPERTY(bool, AltGrAliasing, true); + WINRT_PROPERTY(til::color, CursorColor, DEFAULT_CURSOR_COLOR); + WINRT_PROPERTY(winrt::Microsoft::Terminal::Core::CursorStyle, CursorShape, winrt::Microsoft::Terminal::Core::CursorStyle::Vintage); + WINRT_PROPERTY(uint32_t, CursorHeight, DEFAULT_CURSOR_HEIGHT); + WINRT_PROPERTY(winrt::hstring, WordDelimiters, DEFAULT_WORD_DELIMITERS); + WINRT_PROPERTY(bool, CopyOnSelect, false); + WINRT_PROPERTY(bool, InputServiceWarning, true); + WINRT_PROPERTY(bool, FocusFollowMouse, false); + + WINRT_PROPERTY(winrt::Windows::Foundation::IReference, TabColor, nullptr); + + WINRT_PROPERTY(winrt::Windows::Foundation::IReference, StartingTabColor, nullptr); + + winrt::Microsoft::Terminal::Core::ICoreAppearance UnfocusedAppearance() { return {}; }; + + WINRT_PROPERTY(bool, TrimBlockSelection, false); + // ------------------------ End of Core Settings ----------------------- + + WINRT_PROPERTY(winrt::hstring, ProfileName); + WINRT_PROPERTY(bool, UseAcrylic, false); + WINRT_PROPERTY(double, TintOpacity, 0.5); + WINRT_PROPERTY(winrt::hstring, Padding, DEFAULT_PADDING); + WINRT_PROPERTY(winrt::hstring, FontFace, L"Consolas"); + WINRT_PROPERTY(int32_t, FontSize, DEFAULT_FONT_SIZE); + + WINRT_PROPERTY(winrt::Windows::UI::Text::FontWeight, FontWeight); + + WINRT_PROPERTY(winrt::hstring, BackgroundImage); + WINRT_PROPERTY(double, BackgroundImageOpacity, 1.0); + + WINRT_PROPERTY(winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill); + WINRT_PROPERTY(winrt::Windows::UI::Xaml::HorizontalAlignment, BackgroundImageHorizontalAlignment, winrt::Windows::UI::Xaml::HorizontalAlignment::Center); + WINRT_PROPERTY(winrt::Windows::UI::Xaml::VerticalAlignment, BackgroundImageVerticalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment::Center); + + WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::IKeyBindings, KeyBindings, nullptr); + + WINRT_PROPERTY(winrt::hstring, Commandline); + WINRT_PROPERTY(winrt::hstring, StartingDirectory); + WINRT_PROPERTY(winrt::hstring, StartingTitle); + WINRT_PROPERTY(bool, SuppressApplicationTitle); + WINRT_PROPERTY(winrt::hstring, EnvironmentVariables); + + WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible); + + WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale); + + WINRT_PROPERTY(bool, RetroTerminalEffect, false); + WINRT_PROPERTY(bool, ForceFullRepaintRendering, false); + WINRT_PROPERTY(bool, SoftwareRendering, false); + WINRT_PROPERTY(bool, ForceVTInput, false); + + WINRT_PROPERTY(winrt::hstring, PixelShaderPath); + + WINRT_PROPERTY(bool, DetectURLs, true); + + private: + std::array _ColorTable; + + public: + winrt::Microsoft::Terminal::Core::Color GetColorTableEntry(int32_t index) noexcept { return _ColorTable.at(index); } + std::array ColorTable() { return _ColorTable; } + void ColorTable(std::array /*colors*/) {} + }; +} + +namespace winrt::SampleApp::factory_implementation +{ + BASIC_FACTORY(MySettings); +} diff --git a/scratch/ScratchIslandApp/SampleApp/MySettings.idl b/scratch/ScratchIslandApp/SampleApp/MySettings.idl new file mode 100644 index 000000000..42422b21a --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/MySettings.idl @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace SampleApp +{ + [default_interface] runtimeclass MySettings : Microsoft.Terminal.Core.ICoreSettings, + Microsoft.Terminal.Control.IControlSettings, + Microsoft.Terminal.Core.ICoreAppearance, + Microsoft.Terminal.Control.IControlAppearance + + { + MySettings(); + } +} diff --git a/scratch/ScratchIslandApp/SampleApp/Resources/en-US/Resources.resw b/scratch/ScratchIslandApp/SampleApp/Resources/en-US/Resources.resw new file mode 100644 index 000000000..f4af46df5 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/Resources/en-US/Resources.resw @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj b/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj new file mode 100644 index 000000000..c16d6be6f --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj @@ -0,0 +1,181 @@ + + + + {a4394404-37f7-41c1-802b-49788d3720e3} + Win32Proj + SampleApp + SampleAppLib + SampleAppLib + StaticLibrary + Console + true + + false + nested + + + + + + + + + true + + + + + + + + Designer + + + + + + Designer + + + + + + + + MyPage.xaml + Code + + + App.xaml + + + SampleAppLogic.idl + + + + + + + + MyPage.xaml + Code + + + Create + + + App.xaml + + + SampleAppLogic.idl + + + + + + + + + App.xaml + + + + + MyPage.xaml + Code + + + + + + + + + + + + Warning + + + + + + $(OpenConsoleCommonOutDir)TerminalCore\Microsoft.Terminal.Core.winmd + true + false + false + + + $(OpenConsoleCommonOutDir)TerminalConnection\Microsoft.Terminal.TerminalConnection.winmd + true + false + false + + + $(OpenConsoleCommonOutDir)Microsoft.Terminal.Control\Microsoft.Terminal.Control.winmd + true + false + false + + + + + + pch.h + ..;%(AdditionalIncludeDirectories); + + 4702;%(DisableSpecificWarnings) + + + $(OpenConsoleCommonOutDir)\ConTypes.lib;WindowsApp.lib;shell32.lib;%(AdditionalDependencies) + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + <_GenerateProjectPriFileDependsOn>OpenConsolePlaceAppXbfAtRootOfResourceTree;$(_GenerateProjectPriFileDependsOn) + + + + <_RelocatedAppXamlData Include="@(PackagingOutputs)" Condition="'%(Filename)' == 'App' and ('%(Extension)' == '.xaml' or '%(Extension)' == '.xbf')" /> + + + %(Filename)%(Extension) + + + + + + diff --git a/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.cpp b/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.cpp new file mode 100644 index 000000000..4498f70df --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "SampleAppLogic.h" +#include "SampleAppLogic.g.cpp" + +#include + +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::System; + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +namespace winrt::SampleApp::implementation +{ + // Function Description: + // - Get the SampleAppLogic for the current active Xaml application, or null if there isn't one. + // Return value: + // - A pointer (bare) to the SampleAppLogic, or nullptr. The app logic outlives all other objects, + // unless the application is in a terrible way, so this is "safe." + SampleAppLogic* SampleAppLogic::Current() noexcept + try + { + if (auto currentXamlApp{ winrt::Windows::UI::Xaml::Application::Current().try_as() }) + { + if (auto SampleAppLogicPointer{ winrt::get_self(currentXamlApp.Logic()) }) + { + return SampleAppLogicPointer; + } + } + return nullptr; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + return nullptr; + } + + SampleAppLogic::SampleAppLogic() + { + // 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, + // then it might look like App just failed to activate, which will + // cause you to chase down the rabbit hole of "why is App not + // registered?" when it definitely is. + + // The MyPage has to be constructed during our construction, to + // make sure that there's a terminal page for callers of + // SetTitleBarContent + _root = winrt::make_self(); + } + + // Method Description: + // - Build the UI for the terminal app. Before this method is called, it + // should not be assumed that the SampleApp is usable. The Settings + // should be loaded before this is called, either with LoadSettings or + // GetLaunchDimensions (which will call LoadSettings) + // Arguments: + // - + // Return Value: + // - + void SampleAppLogic::Create() + { + _root->Create(); + } + + UIElement SampleAppLogic::GetRoot() noexcept + { + return _root.as(); + } + + hstring SampleAppLogic::Title() + { + return _root->Title(); + } + +} diff --git a/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.h b/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.h new file mode 100644 index 000000000..8b8642a8e --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "SampleAppLogic.g.h" +#include "MyPage.h" +#include "../../../src/cascadia/inc/cppwinrt_utils.h" + +namespace winrt::SampleApp::implementation +{ + struct SampleAppLogic : SampleAppLogicT + { + public: + static SampleAppLogic* Current() noexcept; + + SampleAppLogic(); + ~SampleAppLogic() = default; + + void Create(); + + Windows::UI::Xaml::UIElement GetRoot() noexcept; + + winrt::hstring Title(); + + private: + // If you add controls here, but forget to null them either here or in + // the ctor, you're going to have a bad time. It'll mysteriously fail to + // activate the SampleAppLogic. + // ALSO: If you add any UIElements as roots here, make sure they're + // updated in _ApplyTheme. The root currently is _root. + winrt::com_ptr _root{ nullptr }; + }; +} + +namespace winrt::SampleApp::factory_implementation +{ + struct SampleAppLogic : SampleAppLogicT + { + }; +} diff --git a/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.idl b/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.idl new file mode 100644 index 000000000..9b7d49e1a --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/SampleAppLogic.idl @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +namespace SampleApp +{ + + [default_interface] runtimeclass SampleAppLogic + { + SampleAppLogic(); + + void Create(); + + Windows.UI.Xaml.UIElement GetRoot(); + + String Title { get; }; + + } +} diff --git a/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.def b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.def new file mode 100644 index 000000000..8c1a02932 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj new file mode 100644 index 000000000..1b1961fac --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj @@ -0,0 +1,101 @@ + + + + {26c51792-41a3-4fe0-ab5e-8b69d557bf91} + SampleApp + SampleApp + + + DynamicLibrary + Console + + true + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + true + true + + + + + + + + $(OpenConsoleCommonOutDir)TerminalCore\Microsoft.Terminal.Core.winmd + true + false + false + + + $(OpenConsoleCommonOutDir)TerminalConnection\Microsoft.Terminal.TerminalConnection.winmd + true + false + false + + + $(OpenConsoleCommonOutDir)Microsoft.Terminal.Control\Microsoft.Terminal.Control.winmd + true + false + false + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + $(OpenConsoleDir)\dep\jsoncpp\json;%(AdditionalIncludeDirectories); + + + WindowsApp.lib;%(AdditionalDependencies) + + /INCLUDE:_DllMain@12 %(AdditionalOptions) + /INCLUDE:DllMain %(AdditionalOptions) + + + + diff --git a/scratch/ScratchIslandApp/SampleApp/dll/pch.cpp b/scratch/ScratchIslandApp/SampleApp/dll/pch.cpp new file mode 100644 index 000000000..3c27d44d5 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/dll/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/scratch/ScratchIslandApp/SampleApp/dll/pch.h b/scratch/ScratchIslandApp/SampleApp/dll/pch.h new file mode 100644 index 000000000..76004c88f --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/dll/pch.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// pch.h +// Header for platform projection include files +// + +#pragma once + +// This file can be empty - the pch.h in SampleApp/lib does the heavy lifting +// of including all the headers we need. As this project is just a dll wrapper, +// we don't actually need anything in here. diff --git a/scratch/ScratchIslandApp/SampleApp/init.cpp b/scratch/ScratchIslandApp/SampleApp/init.cpp new file mode 100644 index 000000000..1c169d9d9 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/init.cpp @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +#include "pch.h" +#include +#include + +BOOL WINAPI DllMain(HINSTANCE /*hInstDll*/, DWORD /*reason*/, LPVOID /*reserved*/) +{ + return TRUE; +} + +UTILS_DEFINE_LIBRARY_RESOURCE_SCOPE(L"SampleApp/Resources") diff --git a/scratch/ScratchIslandApp/SampleApp/packages.config b/scratch/ScratchIslandApp/SampleApp/packages.config new file mode 100644 index 000000000..a6c68340a --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/packages.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/scratch/ScratchIslandApp/SampleApp/pch.cpp b/scratch/ScratchIslandApp/SampleApp/pch.cpp new file mode 100644 index 000000000..3c27d44d5 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/scratch/ScratchIslandApp/SampleApp/pch.h b/scratch/ScratchIslandApp/SampleApp/pch.h new file mode 100644 index 000000000..fd9d893f9 --- /dev/null +++ b/scratch/ScratchIslandApp/SampleApp/pch.h @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// pch.h +// Header for platform projection include files +// + +#pragma once + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOHELP +#define NOCOMM + +// Manually include til after we include Windows.Foundation to give it winrt superpowers +#define BLOCK_TIL +#include +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "winrt/Windows.UI.Xaml.Markup.h" +#include "winrt/Windows.UI.ViewManagement.h" + +#include +#include +#include +#include + +#include + +// Including TraceLogging essentials for the binary +#include +#include +TRACELOGGING_DECLARE_PROVIDER(g_hSampleAppProvider); +#include +#include + +#include +#include + +#include +#include +#include + +// Manually include til after we include Windows.Foundation to give it winrt superpowers +#include "til.h" diff --git a/scratch/ScratchIslandApp/WindowExe/SampleAppHost.cpp b/scratch/ScratchIslandApp/WindowExe/SampleAppHost.cpp new file mode 100644 index 000000000..2d3afefc5 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/SampleAppHost.cpp @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "SampleAppHost.h" +#include "../types/inc/Viewport.hpp" +#include "../types/inc/utils.hpp" +#include "../types/inc/User32Utils.hpp" +#include "resource.h" + +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Composition; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Hosting; +using namespace winrt::Windows::Foundation::Numerics; +using namespace ::Microsoft::Console; +using namespace ::Microsoft::Console::Types; + +SampleAppHost::SampleAppHost() noexcept : + _app{}, + _logic{ nullptr }, // don't make one, we're going to take a ref on app's + _window{ nullptr } +{ + _logic = _app.Logic(); // get a ref to app's logic + + _window = std::make_unique(); + _window->MakeWindow(); +} + +SampleAppHost::~SampleAppHost() +{ + // destruction order is important for proper teardown here + _window = nullptr; + _app.Close(); + _app = nullptr; +} +// Method Description: +// - Initializes the XAML island, creates the terminal app, and sets the +// island's content to that of the terminal app's content. Also registers some +// callbacks with TermApp. +// !!! IMPORTANT!!! +// This must be called *AFTER* WindowsXamlManager::InitializeForCurrentThread. +// If it isn't, then we won't be able to create the XAML island. +// Arguments: +// - +// Return Value: +// - +void SampleAppHost::Initialize() +{ + _window->Initialize(); + + _logic.Create(); + + _window->UpdateTitle(_logic.Title()); + + // Set up the content of the application. If the app has a custom titlebar, + // set that content as well. + _window->SetContent(_logic.GetRoot()); + + _window->OnAppInitialized(); + + // THIS IS A HACK + // + // We've got a weird crash that happens terribly inconsistently, only in + // Debug mode. Apparently, there's some weird ref-counting magic that goes + // on during teardown, and our Application doesn't get closed quite right, + // which can cause us to crash into the debugger. This of course, only + // happens on exit, and happens somewhere in the XamlHost.dll code. + // + // Crazily, if we _manually leak the Application_ here, then the crash + // doesn't happen. This doesn't matter, because we really want the + // Application to live for _the entire lifetime of the process_, so the only + // time when this object would actually need to get cleaned up is _during + // exit_. So we can safely leak this Application object, and have it just + // get cleaned up normally when our process exits. + ::winrt::SampleApp::App a{ _app }; + ::winrt::detach_abi(a); +} diff --git a/scratch/ScratchIslandApp/WindowExe/SampleAppHost.h b/scratch/ScratchIslandApp/WindowExe/SampleAppHost.h new file mode 100644 index 000000000..226f90a4b --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/SampleAppHost.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "SampleIslandWindow.h" + +class SampleAppHost +{ +public: + SampleAppHost() noexcept; + virtual ~SampleAppHost(); + + void Initialize(); + +private: + std::unique_ptr _window; + winrt::SampleApp::App _app; + winrt::SampleApp::SampleAppLogic _logic; +}; diff --git a/scratch/ScratchIslandApp/WindowExe/SampleBaseWindow.h b/scratch/ScratchIslandApp/WindowExe/SampleBaseWindow.h new file mode 100644 index 000000000..5c25452b3 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/SampleBaseWindow.h @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// Custom window messages +#define CM_UPDATE_TITLE (WM_USER) + +#include + +template +class BaseWindow +{ +public: + virtual ~BaseWindow() = 0; + static T* GetThisFromHandle(HWND const window) noexcept + { + return reinterpret_cast(GetWindowLongPtr(window, GWLP_USERDATA)); + } + + [[nodiscard]] static LRESULT __stdcall WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept + { + WINRT_ASSERT(window); + + if (WM_NCCREATE == message) + { + auto cs = reinterpret_cast(lparam); + T* that = static_cast(cs->lpCreateParams); + WINRT_ASSERT(that); + WINRT_ASSERT(!that->_window); + that->_window = wil::unique_hwnd(window); + + return that->_OnNcCreate(wparam, lparam); + } + else if (T* that = GetThisFromHandle(window)) + { + return that->MessageHandler(message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); + } + + [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept + { + switch (message) + { + case WM_DPICHANGED: + { + return HandleDpiChange(_window.get(), wparam, lparam); + } + + case WM_DESTROY: + { + PostQuitMessage(0); + return 0; + } + + case WM_SIZE: + { + UINT width = LOWORD(lparam); + UINT height = HIWORD(lparam); + + switch (wparam) + { + case SIZE_MAXIMIZED: + [[fallthrough]]; + case SIZE_RESTORED: + if (_minimized) + { + _minimized = false; + OnRestore(); + } + + // We always need to fire the resize event, even when we're transitioning from minimized. + // We might be transitioning directly from minimized to maximized, and we'll need + // to trigger any size-related content changes. + OnResize(width, height); + break; + case SIZE_MINIMIZED: + if (!_minimized) + { + _minimized = true; + OnMinimize(); + } + break; + default: + // do nothing. + break; + } + break; + } + case CM_UPDATE_TITLE: + { + SetWindowTextW(_window.get(), _title.c_str()); + break; + } + } + + return DefWindowProc(_window.get(), message, wparam, lparam); + } + + // DPI Change handler. on WM_DPICHANGE resize the window + [[nodiscard]] LRESULT HandleDpiChange(const HWND hWnd, const WPARAM wParam, const LPARAM lParam) + { + _inDpiChange = true; + const HWND hWndStatic = GetWindow(hWnd, GW_CHILD); + if (hWndStatic != nullptr) + { + const UINT uDpi = HIWORD(wParam); + + // Resize the window + auto lprcNewScale = reinterpret_cast(lParam); + + SetWindowPos(hWnd, nullptr, lprcNewScale->left, lprcNewScale->top, lprcNewScale->right - lprcNewScale->left, lprcNewScale->bottom - lprcNewScale->top, SWP_NOZORDER | SWP_NOACTIVATE); + + _currentDpi = uDpi; + } + _inDpiChange = false; + return 0; + } + + virtual void OnResize(const UINT width, const UINT height) = 0; + virtual void OnMinimize() = 0; + virtual void OnRestore() = 0; + + RECT GetWindowRect() const noexcept + { + RECT rc = { 0 }; + ::GetWindowRect(_window.get(), &rc); + return rc; + } + + HWND GetHandle() const noexcept + { + return _window.get(); + } + + float GetCurrentDpiScale() const noexcept + { + const auto dpi = ::GetDpiForWindow(_window.get()); + const auto scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + return scale; + } + + // Gets the physical size of the client area of the HWND in _window + SIZE GetPhysicalSize() const noexcept + { + RECT rect = {}; + GetClientRect(_window.get(), &rect); + const auto windowsWidth = rect.right - rect.left; + const auto windowsHeight = rect.bottom - rect.top; + return SIZE{ windowsWidth, windowsHeight }; + } + + // Gets the logical (in DIPs) size of a physical size specified by the parameter physicalSize + // Remarks: + // XAML coordinate system is always in Display Independent Pixels (a.k.a DIPs or Logical). However Win32 GDI (because of legacy reasons) + // in DPI mode "Per-Monitor and Per-Monitor (V2) DPI Awareness" is always in physical pixels. + // The formula to transform is: + // logical = (physical / dpi) + 0.5 // 0.5 is to ensure that we pixel snap correctly at the edges, this is necessary with odd DPIs like 1.25, 1.5, 1, .75 + // See also: + // https://docs.microsoft.com/en-us/windows/desktop/LearnWin32/dpi-and-device-independent-pixels + // https://docs.microsoft.com/en-us/windows/desktop/hidpi/high-dpi-desktop-application-development-on-windows#per-monitor-and-per-monitor-v2-dpi-awareness + winrt::Windows::Foundation::Size GetLogicalSize(const SIZE physicalSize) const noexcept + { + const auto scale = GetCurrentDpiScale(); + // 0.5 is to ensure that we pixel snap correctly at the edges, this is necessary with odd DPIs like 1.25, 1.5, 1, .75 + const auto logicalWidth = (physicalSize.cx / scale) + 0.5f; + const auto logicalHeight = (physicalSize.cy / scale) + 0.5f; + return winrt::Windows::Foundation::Size(logicalWidth, logicalHeight); + } + + winrt::Windows::Foundation::Size GetLogicalSize() const noexcept + { + return GetLogicalSize(GetPhysicalSize()); + } + + // Method Description: + // - Sends a message to our message loop to update the title of the window. + // Arguments: + // - newTitle: a string to use as the new title of the window. + // Return Value: + // - + void UpdateTitle(std::wstring_view newTitle) + { + _title = newTitle; + PostMessageW(_window.get(), CM_UPDATE_TITLE, 0, reinterpret_cast(nullptr)); + } + + // Method Description: + // Reset the current dpi of the window. This method is only called after we change the + // initial launch position. This makes sure the dpi is consistent with the monitor on which + // the window will launch + void RefreshCurrentDPI() + { + _currentDpi = GetDpiForWindow(_window.get()); + } + +protected: + using base_type = BaseWindow; + wil::unique_hwnd _window; + + unsigned int _currentDpi = 0; + bool _inDpiChange = false; + + std::wstring _title = L""; + + bool _minimized = false; + + // Method Description: + // - This method is called when the window receives the WM_NCCREATE message. + // Return Value: + // - The value returned from the window proc. + virtual [[nodiscard]] LRESULT _OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept + { + SetWindowLongPtr(_window.get(), GWLP_USERDATA, reinterpret_cast(this)); + + EnableNonClientDpiScaling(_window.get()); + _currentDpi = GetDpiForWindow(_window.get()); + + return DefWindowProc(_window.get(), WM_NCCREATE, wParam, lParam); + }; +}; + +template +inline BaseWindow::~BaseWindow() +{ +} diff --git a/scratch/ScratchIslandApp/WindowExe/SampleIslandWindow.cpp b/scratch/ScratchIslandApp/WindowExe/SampleIslandWindow.cpp new file mode 100644 index 000000000..1c5cf10d0 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/SampleIslandWindow.cpp @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "SampleIslandWindow.h" +#include "../types/inc/Viewport.hpp" +#include "resource.h" +#include "icon.h" + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Composition; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Hosting; +using namespace winrt::Windows::Foundation::Numerics; +using namespace ::Microsoft::Console::Types; + +#define XAML_HOSTING_WINDOW_CLASS_NAME L"SCRATCH_HOSTING_WINDOW_CLASS" + +SampleIslandWindow::SampleIslandWindow() noexcept : + _interopWindowHandle{ nullptr }, + _rootGrid{ nullptr }, + _source{ nullptr } +{ +} + +SampleIslandWindow::~SampleIslandWindow() +{ + _source.Close(); +} + +// Method Description: +// - Create the actual window that we'll use for the application. +// Arguments: +// - +// Return Value: +// - +void SampleIslandWindow::MakeWindow() noexcept +{ + WNDCLASS wc{}; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hInstance = reinterpret_cast(&__ImageBase); + wc.lpszClassName = XAML_HOSTING_WINDOW_CLASS_NAME; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WndProc; + wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(IDI_APPICON)); + RegisterClass(&wc); + WINRT_ASSERT(!_window); + + // Create the window with the default size here - During the creation of the + // window, the system will give us a chance to set its size in WM_CREATE. + // WM_CREATE will be handled synchronously, before CreateWindow returns. + WINRT_VERIFY(CreateWindowEx(0, + wc.lpszClassName, + L"ScratchApp", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + nullptr, + nullptr, + wc.hInstance, + this)); + + WINRT_ASSERT(_window); +} + +// Method Description: +// - Called when no tab is remaining to close the window. +// Arguments: +// - +// Return Value: +// - +void SampleIslandWindow::Close() +{ + PostQuitMessage(0); +} + +// Method Description: +// - Handles a WM_CREATE message. Calls our create callback, if one's been set. +// Arguments: +// - wParam: unused +// - lParam: the lParam of a WM_CREATE, which is a pointer to a CREATESTRUCTW +// Return Value: +// - +void SampleIslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexcept +{ + // Get proposed window rect from create structure + CREATESTRUCTW* pcs = reinterpret_cast(lParam); + RECT rc; + rc.left = pcs->x; + rc.top = pcs->y; + rc.right = rc.left + pcs->cx; + rc.bottom = rc.top + pcs->cy; + + ShowWindow(_window.get(), SW_SHOW); + + UpdateWindow(_window.get()); + + UpdateWindowIconForActiveMetrics(_window.get()); +} + +void SampleIslandWindow::Initialize() +{ + const bool initialized = (_interopWindowHandle != nullptr); + + _source = DesktopWindowXamlSource{}; + + auto interop = _source.as(); + winrt::check_hresult(interop->AttachToWindow(_window.get())); + + // stash the child interop handle so we can resize it when the main hwnd is resized + interop->get_WindowHandle(&_interopWindowHandle); + + _rootGrid = winrt::Windows::UI::Xaml::Controls::Grid(); + _source.Content(_rootGrid); +} + +void SampleIslandWindow::OnSize(const UINT width, const UINT height) +{ + // update the interop window size + SetWindowPos(_interopWindowHandle, nullptr, 0, 0, width, height, SWP_SHOWWINDOW | SWP_NOACTIVATE); + + if (_rootGrid) + { + const auto size = GetLogicalSize(); + _rootGrid.Width(size.Width); + _rootGrid.Height(size.Height); + } +} + +[[nodiscard]] LRESULT SampleIslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept +{ + switch (message) + { + case WM_CREATE: + { + _HandleCreateWindow(wparam, lparam); + return 0; + } + case WM_SETFOCUS: + { + if (_interopWindowHandle != nullptr) + { + // send focus to the child window + SetFocus(_interopWindowHandle); + return 0; + } + break; + } + case WM_MENUCHAR: + { + // GH#891: return this LRESULT here to prevent the app from making a + // bell when alt+key is pressed. A menu is active and the user presses a + // key that does not correspond to any mnemonic or accelerator key, + return MAKELRESULT(0, MNC_CLOSE); + } + case WM_THEMECHANGED: + UpdateWindowIconForActiveMetrics(_window.get()); + return 0; + } + + return base_type::MessageHandler(message, wparam, lparam); +} + +// Method Description: +// - Called when the window has been resized (or maximized) +// Arguments: +// - width: the new width of the window _in pixels_ +// - height: the new height of the window _in pixels_ +void SampleIslandWindow::OnResize(const UINT width, const UINT height) +{ + if (_interopWindowHandle) + { + OnSize(width, height); + } +} + +// Method Description: +// - Called when the window is minimized to the taskbar. +void SampleIslandWindow::OnMinimize() +{ +} + +// Method Description: +// - Called when the window is restored from having been minimized. +void SampleIslandWindow::OnRestore() +{ +} + +void SampleIslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) +{ + _rootGrid.Children().Clear(); + _rootGrid.Children().Append(content); +} + +void SampleIslandWindow::OnAppInitialized() +{ + // Do a quick resize to force the island to paint + const auto size = GetPhysicalSize(); + OnSize(size.cx, size.cy); +} + +// Method Description: +// - Called when the app wants to change its theme. We'll update the root UI +// element of the entire XAML tree, so that all UI elements get the theme +// applied. +// Arguments: +// - arg: the ElementTheme to use as the new theme for the UI +// Return Value: +// - +void SampleIslandWindow::OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) +{ + _rootGrid.RequestedTheme(requestedTheme); + // Invalidate the window rect, so that we'll repaint any elements we're + // drawing ourselves to match the new theme + ::InvalidateRect(_window.get(), nullptr, false); +} diff --git a/scratch/ScratchIslandApp/WindowExe/SampleIslandWindow.h b/scratch/ScratchIslandApp/WindowExe/SampleIslandWindow.h new file mode 100644 index 000000000..14a62f13c --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/SampleIslandWindow.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "SampleBaseWindow.h" +#include "../../../src/cascadia/inc/cppwinrt_utils.h" + +class SampleIslandWindow : + public BaseWindow +{ +public: + SampleIslandWindow() noexcept; + virtual ~SampleIslandWindow() override; + + virtual void MakeWindow() noexcept; + void Close(); + + virtual void OnSize(const UINT width, const UINT height); + + [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; + void OnResize(const UINT width, const UINT height) override; + void OnMinimize() override; + void OnRestore() override; + virtual void OnAppInitialized(); + virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); + virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + + virtual void Initialize(); + +protected: + void ForceResize() + { + // Do a quick resize to force the island to paint + const auto size = GetPhysicalSize(); + OnSize(size.cx, size.cy); + } + + HWND _interopWindowHandle; + + winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; + + winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; + + void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; +}; diff --git a/scratch/ScratchIslandApp/WindowExe/SampleMain.cpp b/scratch/ScratchIslandApp/WindowExe/SampleMain.cpp new file mode 100644 index 000000000..4c09f262c --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/SampleMain.cpp @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "SampleAppHost.h" +#include "resource.h" +#include "../types/inc/User32Utils.hpp" +#include + +using namespace winrt; +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Composition; +using namespace winrt::Windows::UI::Xaml::Hosting; +using namespace winrt::Windows::Foundation::Numerics; + +// Routine Description: +// - Takes an image architecture and locates a string resource that maps to that architecture. +// Arguments: +// - imageArchitecture - An IMAGE_FILE_MACHINE architecture enum value +// - See https://docs.microsoft.com/en-us/windows/win32/sysinfo/image-file-machine-constants +// Return Value: +// - A string value representing the human-readable name of this architecture. +static std::wstring ImageArchitectureToString(USHORT imageArchitecture) +{ + // clang-format off + const auto id = imageArchitecture == IMAGE_FILE_MACHINE_I386 ? IDS_X86_ARCHITECTURE : + imageArchitecture == IMAGE_FILE_MACHINE_AMD64 ? IDS_AMD64_ARCHITECTURE : + imageArchitecture == IMAGE_FILE_MACHINE_ARM64 ? IDS_ARM64_ARCHITECTURE : + imageArchitecture == IMAGE_FILE_MACHINE_ARM ? IDS_ARM_ARCHITECTURE : + IDS_UNKNOWN_ARCHITECTURE; + // clang-format on + + return GetStringResource(id); +} + +// Routine Description: +// - Blocks the user from launching the application with a message box dialog and early exit +// if the process architecture doesn't match the system platform native architecture. +// - This is because the conhost.exe must match the condrv.sys on the system and the PTY +// infrastructure that powers everything won't work if we have a mismatch. +// Arguments: +// - +// Return Value: +// - +static void EnsureNativeArchitecture() +{ + USHORT processMachine{}; + USHORT nativeMachine{}; + THROW_IF_WIN32_BOOL_FALSE(IsWow64Process2(GetCurrentProcess(), &processMachine, &nativeMachine)); + if (processMachine != IMAGE_FILE_MACHINE_UNKNOWN && processMachine != nativeMachine) + { + const auto formatPattern = GetStringResource(IDS_ERROR_ARCHITECTURE_FORMAT); + + const auto nativeArchitecture = ImageArchitectureToString(nativeMachine); + const auto processArchitecture = ImageArchitectureToString(processMachine); + + auto buffer{ wil::str_printf(formatPattern.data(), nativeArchitecture.data(), processArchitecture.data()) }; + + MessageBoxW(nullptr, + buffer.data(), + GetStringResource(IDS_ERROR_DIALOG_TITLE).data(), + MB_OK | MB_ICONERROR); + + ExitProcess(0); + } +} + +static bool _messageIsF7Keypress(const MSG& message) +{ + return (message.message == WM_KEYDOWN || message.message == WM_SYSKEYDOWN) && message.wParam == VK_F7; +} +static bool _messageIsAltKeyup(const MSG& message) +{ + return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU; +} + +int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) +{ + // If Terminal is spawned by a shortcut that requests that it run in a new process group + // while attached to a console session, that request is nonsense. That request will, however, + // cause WT to start with Ctrl-C disabled. This wouldn't matter, because it's a Windows-subsystem + // application. Unfortunately, that state is heritable. In short, if you start WT using cmd in + // a weird way, ^C stops working _inside_ the terminal. Mad. + SetConsoleCtrlHandler(NULL, FALSE); + + // Block the user from starting if they launched the incorrect architecture version of the project. + // This should only be applicable to developer versions. The package installation process + // should choose and install the correct one from the bundle. + EnsureNativeArchitecture(); + + // Make sure to call this so we get WM_POINTER messages. + EnableMouseInPointer(true); + + // !!! LOAD BEARING !!! + // We must initialize the main thread as a single-threaded apartment before + // constructing any Xaml objects. Failing to do so will cause some issues + // in accessibility somewhere down the line when a UIAutomation object will + // be queried on the wrong thread at the wrong time. + // We used to initialize as STA only _after_ initializing the application + // host, which loaded the settings. The settings needed to be loaded in MTA + // because we were using the Windows.Storage APIs. Since we're no longer + // doing that, we can safely init as STA before any WinRT dispatches. + winrt::init_apartment(winrt::apartment_type::single_threaded); + + // Create the SampleAppHost object, which will create both the window and the + // Terminal App. This MUST BE constructed before the Xaml manager as TermApp + // provides an implementation of Windows.UI.Xaml.Application. + SampleAppHost host; + + // Initialize the xaml content. This must be called AFTER the + // WindowsXamlManager is initialized. + host.Initialize(); + + MSG message; + + while (GetMessage(&message, nullptr, 0, 0)) + { + TranslateMessage(&message); + DispatchMessage(&message); + } + return 0; +} diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.def b/scratch/ScratchIslandApp/WindowExe/WindowExe.def new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.def @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.manifest b/scratch/ScratchIslandApp/WindowExe/WindowExe.manifest new file mode 100644 index 000000000..9d91e543d --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + PerMonitorV2 + true + + + diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.rc b/scratch/ScratchIslandApp/WindowExe/WindowExe.rc new file mode 100644 index 000000000..b11a8c88d --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.rc @@ -0,0 +1,97 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. + +IDI_APPICON ICON "..\\..\\..\\res\\terminal.ico" +IDI_APPICON_HC_BLACK ICON "..\\..\\..\\res\\terminal\\images\\terminal_contrast-black.ico" +IDI_APPICON_HC_WHITE ICON "..\\..\\..\\res\\terminal\\images\\terminal_contrast-white.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_ERROR_DIALOG_TITLE "Error" + IDS_HELP_DIALOG_TITLE "Help" + IDS_ERROR_ARCHITECTURE_FORMAT + "This sample is designed to run on your system's native architecture (%s).\nYou are currently using the %s version.\n\nPlease use the version of this sample that matches your system's native architecture." + IDS_X86_ARCHITECTURE "i386" +END + +STRINGTABLE +BEGIN + IDS_AMD64_ARCHITECTURE "AMD64" + IDS_ARM64_ARCHITECTURE "ARM64" + IDS_ARM_ARCHITECTURE "ARM" + IDS_UNKNOWN_ARCHITECTURE "Unknown" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj new file mode 100644 index 000000000..edd372c2f --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj @@ -0,0 +1,220 @@ + + + + + + {b4427499-9fde-4208-b456-5bc580637633} + Win32Proj + WindowExe + WindowExe + WindowExe + Application + false + Windows Store + true + false + Windows + + + + + + + + true + + + + + + $(OpenConsoleDir)\src\inc;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\Console;$(OpenConsoleDir)\dep\Win32K;$(OpenConsoleDir)\dep\gsl\include;%(AdditionalIncludeDirectories); + + + %(AdditionalDependencies) + + + + true + true + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + + + + + $(OpenConsoleCommonOutDir)TerminalCore\Microsoft.Terminal.Core.winmd + true + true + true + + + $(OpenConsoleCommonOutDir)TerminalConnection\Microsoft.Terminal.TerminalConnection.winmd + $(OpenConsoleCommonOutDir)TerminalConnection\TerminalConnection.dll + true + true + true + + + $(OpenConsoleCommonOutDir)Microsoft.Terminal.Control\Microsoft.Terminal.Control.winmd + $(OpenConsoleCommonOutDir)Microsoft.Terminal.Control\Microsoft.Terminal.Control.dll + true + true + true + + + + + + + + + WindowsLocalDebugger + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + <_ContinueOnError Condition="'$(BuildingProject)' == 'true'">true + <_ContinueOnError Condition="'$(BuildingProject)' != 'true'">false + + + + + + + + + + + + + x86 + $(Platform) + + + + + <_OpenConsoleVCLibToCopy Include="$(VCToolsRedistInstallDir)\$(ReasonablePlatform)\Microsoft.VC142.CRT\*.dll" /> + + + $(ProjectName) + BuiltProjectOutputGroup + %(Filename)%(Extension) + + + + + + + + + <_TerminalConnectionDlls Include="$(OpenConsoleCommonOutDir)\TerminalConnection\*.dll" /> + + + $(ProjectName) + BuiltProjectOutputGroup + %(Filename)%(Extension) + + + + + + + <_WindowsTerminalExe Include="$(OpenConsoleCommonOutDir)\WindowsTerminal\*.exe" /> + + + $(ProjectName) + BuiltProjectOutputGroup + %(Filename)%(Extension) + + + + + + + + + + + + diff --git a/scratch/ScratchIslandApp/WindowExe/icon.cpp b/scratch/ScratchIslandApp/WindowExe/icon.cpp new file mode 100644 index 000000000..ca194de06 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/icon.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "resource.h" + +static int _GetActiveAppIconResource() +{ + auto iconResource{ IDI_APPICON }; + + HIGHCONTRASTW hcInfo{}; + hcInfo.cbSize = sizeof(hcInfo); + + if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(hcInfo), &hcInfo, 0)) + { + if (WI_IsFlagSet(hcInfo.dwFlags, HCF_HIGHCONTRASTON)) + { + iconResource = IDI_APPICON_HC_BLACK; + + if (0x00FFFFFF == GetSysColor(COLOR_WINDOW)) // white window color == white high contrast + { + iconResource = IDI_APPICON_HC_WHITE; + } + } + } + + return iconResource; +} + +void UpdateWindowIconForActiveMetrics(HWND window) +{ + auto iconResource{ MAKEINTRESOURCEW(_GetActiveAppIconResource()) }; + + // These handles are loaded with LR_SHARED, so they are safe to "leak". + HANDLE smallIcon{ LoadImageW(wil::GetModuleInstanceHandle(), iconResource, IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_SHARED) }; + LOG_LAST_ERROR_IF_NULL(smallIcon); + + HANDLE largeIcon{ LoadImageW(wil::GetModuleInstanceHandle(), iconResource, IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_SHARED) }; + LOG_LAST_ERROR_IF_NULL(largeIcon); + + if (smallIcon) + { + SendMessageW(window, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); + } + if (largeIcon) + { + SendMessageW(window, WM_SETICON, ICON_BIG, reinterpret_cast(largeIcon)); + } +} diff --git a/scratch/ScratchIslandApp/WindowExe/icon.h b/scratch/ScratchIslandApp/WindowExe/icon.h new file mode 100644 index 000000000..5e418fc28 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/icon.h @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +void UpdateWindowIconForActiveMetrics(HWND window); diff --git a/scratch/ScratchIslandApp/WindowExe/packages.config b/scratch/ScratchIslandApp/WindowExe/packages.config new file mode 100644 index 000000000..409baff13 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/packages.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/scratch/ScratchIslandApp/WindowExe/pch.cpp b/scratch/ScratchIslandApp/WindowExe/pch.cpp new file mode 100644 index 000000000..398a99f66 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/scratch/ScratchIslandApp/WindowExe/pch.h b/scratch/ScratchIslandApp/WindowExe/pch.h new file mode 100644 index 000000000..de08907f8 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/pch.h @@ -0,0 +1,82 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- pch.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#pragma once + +// Ignore checked iterators warning from VC compiler. +#define _SCL_SECURE_NO_WARNINGS + +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOHELP +#define NOCOMM + +#include + +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +#include +#include +#include +#include +#include +#include +#include + +// Manually include til after we include Windows.Foundation to give it winrt superpowers +#define BLOCK_TIL +#include "../inc/LibraryIncludes.h" + +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +#include + +// Needed just for XamlIslands to work at all: +#include +#include +#include +#include + +// Additional headers for various xaml features. We need: +// * Core so we can resume_foreground with CoreDispatcher +// * Controls for grid +// * Media for ScaleTransform +#include +#include +#include + +#include + +#include +#include + +// Including TraceLogging essentials for the binary +#include +#include +TRACELOGGING_DECLARE_PROVIDER(g_hWindowsTerminalProvider); +#include +#include + +// For commandline argument processing +#include +#include +#include +#include "til.h" diff --git a/scratch/ScratchIslandApp/WindowExe/resource.h b/scratch/ScratchIslandApp/WindowExe/resource.h new file mode 100644 index 000000000..7e2918600 --- /dev/null +++ b/scratch/ScratchIslandApp/WindowExe/resource.h @@ -0,0 +1,27 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by WindowsTerminal.rc +// +#define IDI_APPICON 101 +#define IDI_APPICON_HC_BLACK 102 +#define IDI_APPICON_HC_WHITE 103 + +#define IDS_ERROR_DIALOG_TITLE 105 +#define IDS_HELP_DIALOG_TITLE 106 +#define IDS_ERROR_ARCHITECTURE_FORMAT 110 +#define IDS_X86_ARCHITECTURE 111 +#define IDS_AMD64_ARCHITECTURE 112 +#define IDS_ARM64_ARCHITECTURE 113 +#define IDS_ARM_ARCHITECTURE 114 +#define IDS_UNKNOWN_ARCHITECTURE 115 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 104 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/buffer/out/sources.inc b/src/buffer/out/sources.inc index 8d5a4ed5f..b78f575b2 100644 --- a/src/buffer/out/sources.inc +++ b/src/buffer/out/sources.inc @@ -30,7 +30,6 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\AttrRow.cpp \ - ..\AttrRowIterator.cpp \ ..\cursor.cpp \ ..\OutputCell.cpp \ ..\OutputCellIterator.cpp \ diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index fe8d0bb6f..f73f4c62f 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -98,7 +98,7 @@ - + diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index edb05aa7b..f4123a10e 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -99,7 +99,7 @@ - + @@ -140,8 +140,10 @@ - + + + diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index 34c943b37..db9cfe825 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -99,7 +99,7 @@ - + --> @@ -140,8 +140,10 @@ - + + + diff --git a/src/cascadia/CascadiaResources.build.items b/src/cascadia/CascadiaResources.build.items index b99fc3ba6..d2d522af8 100644 --- a/src/cascadia/CascadiaResources.build.items +++ b/src/cascadia/CascadiaResources.build.items @@ -12,13 +12,9 @@ Images\%(RecursiveDir)%(FileName)%(Extension) - + true - %(RecursiveDir)%(FileName)%(Extension) - - - true - %(RecursiveDir)%(FileName)%(Extension) + %(FileName)%(Extension) diff --git a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index 35fde481b..bc0a21f72 100644 --- a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -647,10 +647,12 @@ namespace SettingsModelLocalTests const std::string bindings0String{ R"([ { "command": "closeWindow", "keys": "ctrl+a" } ])" }; const std::string bindings1String{ R"([ { "command": { "action": "copy", "singleLine": true }, "keys": "ctrl+b" } ])" }; const std::string bindings2String{ R"([ { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+c" } ])" }; + const std::string bindings3String{ R"([ { "command": "commandPalette", "keys": "ctrl+shift+p" } ])" }; const auto bindings0Json = VerifyParseSucceeded(bindings0String); const auto bindings1Json = VerifyParseSucceeded(bindings1String); const auto bindings2Json = VerifyParseSucceeded(bindings2String); + const auto bindings3Json = VerifyParseSucceeded(bindings3String); auto VerifyKeyChordEquality = [](const KeyChord& expected, const KeyChord& actual) { if (expected) @@ -699,5 +701,13 @@ namespace SettingsModelLocalTests const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::NewTab, *args) }; VerifyKeyChordEquality({ KeyModifiers::Ctrl, static_cast('C') }, kbd); } + { + Log::Comment(L"command with hidden args"); + actionMap->LayerJson(bindings3Json); + VERIFY_ARE_EQUAL(4u, actionMap->_KeyMap.size()); + + const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::ToggleCommandPalette) }; + VerifyKeyChordEquality({ KeyModifiers::Ctrl | KeyModifiers::Shift, static_cast('P') }, kbd); + } } } diff --git a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp index 97251b8b2..164ac0bb8 100644 --- a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp @@ -229,10 +229,12 @@ namespace SettingsModelLocalTests void SerializationTests::Actions() { + // simple command const std::string actionsString1{ R"([ { "command": "paste" } ])" }; + // complex command const std::string actionsString2A{ R"([ { "command": { "action": "setTabColor" } } ])" }; @@ -244,29 +246,35 @@ namespace SettingsModelLocalTests { "command": { "action": "copy", "singleLine": true, "copyFormatting": "html" } } ])" }; + // simple command with key chords const std::string actionsString3{ R"([ { "command": "toggleAlwaysOnTop", "keys": "ctrl+a" }, { "command": "toggleAlwaysOnTop", "keys": "ctrl+b" } ])" }; + // complex command with key chords const std::string actionsString4{ R"([ { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+c" }, { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+d" } ])" }; + // command with name and icon and multiple key chords const std::string actionsString5{ R"([ { "icon": "image.png", "name": "Scroll To Top Name", "command": "scrollToTop", "keys": "ctrl+e" }, { "command": "scrollToTop", "keys": "ctrl+f" } ])" }; + // complex command with new terminal args const std::string actionsString6{ R"([ { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+g" }, ])" }; + // complex command with meaningful null arg const std::string actionsString7{ R"([ { "command": { "action": "renameWindow", "name": null }, "keys": "ctrl+h" } ])" }; + // nested command const std::string actionsString8{ R"([ { "name": "Change font size...", @@ -278,6 +286,7 @@ namespace SettingsModelLocalTests } ])" }; + // iterable command const std::string actionsString9A{ R"([ { "name": "New tab", @@ -330,7 +339,20 @@ namespace SettingsModelLocalTests "name": "Send Input (Evil) ..." } ])"" }; + const std::string actionsString9D{ R""([ + { + "command": + { + "action": "newTab", + "profile": "${profile.name}" + }, + "icon": "${profile.icon}", + "iterateOn": "profiles", + "name": "${profile.name}: New tab" + } + ])"" }; + // unbound command const std::string actionsString10{ R"([ { "command": "unbound", "keys": "ctrl+c" } ])" }; @@ -365,6 +387,7 @@ namespace SettingsModelLocalTests RoundtripTest(actionsString9A); RoundtripTest(actionsString9B); RoundtripTest(actionsString9C); + RoundtripTest(actionsString9D); Log::Comment(L"unbound command"); RoundtripTest(actionsString10); diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest index efb011dba..47612326b 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest @@ -7,10 +7,13 @@ - + - + + + + diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index e25ebf985..53e59bed6 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -798,7 +798,7 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font } } - publicTerminal->_terminal->SetCursorStyle(theme.CursorStyle); + publicTerminal->_terminal->SetCursorStyle(static_cast(theme.CursorStyle)); publicTerminal->_desiredFont = { fontFamily, 0, DEFAULT_FONT_WEIGHT, { 0, fontSize }, CP_UTF8 }; publicTerminal->_UpdateFont(newDpi); diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp index f26f69cca..f88d93794 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp @@ -19,7 +19,7 @@ typedef struct _TerminalTheme COLORREF DefaultForeground; COLORREF DefaultSelectionBackground; float SelectionBackgroundAlpha; - DispatchTypes::CursorStyle CursorStyle; + uint32_t CursorStyle; // This will be converted to DispatchTypes::CursorStyle (size_t), but C# cannot marshal an enum type and have it fit in a size_t. COLORREF ColorTable[16]; } TerminalTheme, *LPTerminalTheme; diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index 60ad959bd..a8f2c70ac 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -4,11 +4,11 @@ #include "pch.h" #include "OpenTerminalHere.h" #include "../WinRTUtils/inc/WtExeUtils.h" +#include "../WinRTUtils/inc/LibraryResources.h" + +#include #include -// TODO GH#6112: Localize these strings -static constexpr std::wstring_view VerbDisplayName{ L"Open in Windows Terminal" }; -static constexpr std::wstring_view VerbDevBuildDisplayName{ L"Open in Windows Terminal (Dev Build)" }; static constexpr std::wstring_view VerbName{ L"WindowsTerminalOpenHere" }; // This code is aggressively copied from @@ -87,8 +87,15 @@ HRESULT OpenTerminalHere::GetTitle(IShellItemArray* /*psiItemArray*/, { // Change the string we return depending on if we're running from the dev // build package or not. - const bool isDevBuild = IsDevBuild(); - return SHStrDup(isDevBuild ? VerbDevBuildDisplayName.data() : VerbDisplayName.data(), ppszName); + const auto resource = +#if defined(WT_BRANDING_RELEASE) + RS_(L"ShellExtension_OpenInTerminalMenuItem"); +#elif defined(WT_BRANDING_PREVIEW) + RS_(L"ShellExtension_OpenInTerminalMenuItem_Preview"); +#else + RS_(L"ShellExtension_OpenInTerminalMenuItem_Dev"); +#endif + return SHStrDup(resource.data(), ppszName); } HRESULT OpenTerminalHere::GetState(IShellItemArray* /*psiItemArray*/, @@ -114,7 +121,7 @@ try std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; modulePath.replace_filename(WindowsTerminalExe); // WindowsTerminal.exe,-101 will be the first icon group in WT - // We're using WindowsTerminal here explicitly, and not wt (from _getExePath), because + // We're using WindowsTerminal here explicitly, and not wt (from GetWtExePath), because // WindowsTerminal is the only one built with the right icons. const auto resource{ modulePath.wstring() + L",-101" }; return SHStrDupW(resource.c_str(), ppszIcon); diff --git a/src/cascadia/ShellExtension/dllmain.cpp b/src/cascadia/ShellExtension/dllmain.cpp index 05c835168..862df6c6a 100644 --- a/src/cascadia/ShellExtension/dllmain.cpp +++ b/src/cascadia/ShellExtension/dllmain.cpp @@ -4,6 +4,8 @@ #include "pch.h" #include "OpenTerminalHere.h" +#include "../WinRTUtils/inc/LibraryResources.h" + using namespace Microsoft::WRL; STDAPI DllCanUnloadNow() @@ -30,3 +32,6 @@ DllMain(_In_opt_ HINSTANCE hinst, DWORD reason, _In_opt_ void*) } return TRUE; } + +// Usurp the TerminalApp's resource group. +UTILS_DEFINE_LIBRARY_RESOURCE_SCOPE(L"TerminalApp/Resources") diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml index 88630a11c..a0678e05e 100644 --- a/src/cascadia/TerminalApp/App.xaml +++ b/src/cascadia/TerminalApp/App.xaml @@ -47,6 +47,9 @@ 8,0,8,0 + + 12 + diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 449d19b6b..e6bf353d5 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -9,6 +9,7 @@ #include #include +#include using namespace winrt::Windows::ApplicationModel; using namespace winrt::Windows::ApplicationModel::DataTransfer; @@ -306,7 +307,8 @@ namespace winrt::TerminalApp::implementation }); _root->Create(); - _ApplyTheme(_settings.GlobalSettings().Theme()); + _ApplyLanguageSettingChange(); + _RefreshThemeRoutine(); _ApplyStartupTaskStateChange(); TraceLoggingWrite( @@ -895,36 +897,46 @@ namespace winrt::TerminalApp::implementation // this stops us from reloading too many times or too quickly. fire_and_forget AppLogic::_DispatchReloadSettings() { - static constexpr auto FileActivityQuiesceTime{ std::chrono::milliseconds(50) }; - if (!_settingsReloadQueued.exchange(true)) + 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() }) { - co_await winrt::resume_after(FileActivityQuiesceTime); _ReloadSettings(); _settingsReloadQueued.store(false); } } - fire_and_forget AppLogic::_LoadErrorsDialogRoutine() + void AppLogic::_ApplyLanguageSettingChange() noexcept + try { - co_await winrt::resume_foreground(_root->Dispatcher()); + if (!IsPackaged()) + { + return; + } - const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle"); - const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText"); - _ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult); + using ApplicationLanguages = winrt::Windows::Globalization::ApplicationLanguages; + + // NOTE: PrimaryLanguageOverride throws if this instance is unpackaged. + const auto primaryLanguageOverride = ApplicationLanguages::PrimaryLanguageOverride(); + const auto language = _settings.GlobalSettings().Language(); + + if (primaryLanguageOverride != language) + { + ApplicationLanguages::PrimaryLanguageOverride(language); + } } + CATCH_LOG() - fire_and_forget AppLogic::_ShowLoadWarningsDialogRoutine() + void AppLogic::_RefreshThemeRoutine() { - co_await winrt::resume_foreground(_root->Dispatcher()); - - _ShowLoadWarningsDialog(); - } - - fire_and_forget AppLogic::_RefreshThemeRoutine() - { - co_await winrt::resume_foreground(_root->Dispatcher()); - - // Refresh the UI theme _ApplyTheme(_settings.GlobalSettings().Theme()); } @@ -959,39 +971,26 @@ namespace winrt::TerminalApp::implementation return; } - auto weakThis{ get_weak() }; - co_await winrt::resume_foreground(_root->Dispatcher(), CoreDispatcherPriority::Normal); - if (auto page{ weakThis.get() }) - { - StartupTaskState state; - bool tryEnableStartupTask = _settings.GlobalSettings().StartOnUserLogin(); - StartupTask task = co_await StartupTask::GetAsync(StartupTaskName); + const auto tryEnableStartupTask = _settings.GlobalSettings().StartOnUserLogin(); + const auto task = co_await StartupTask::GetAsync(StartupTaskName); - state = task.State(); - switch (state) + switch (task.State()) + { + case StartupTaskState::Disabled: + if (tryEnableStartupTask) { - case StartupTaskState::Disabled: + co_await task.RequestEnableAsync(); + } + break; + case StartupTaskState::DisabledByUser: + // TODO: GH#6254: define UX for other StartupTaskStates + break; + case StartupTaskState::Enabled: + if (!tryEnableStartupTask) { - if (tryEnableStartupTask) - { - co_await task.RequestEnableAsync(); - } - break; - } - case StartupTaskState::DisabledByUser: - { - // TODO: GH#6254: define UX for other StartupTaskStates - break; - } - case StartupTaskState::Enabled: - { - if (!tryEnableStartupTask) - { - task.Disable(); - } - break; - } + task.Disable(); } + break; } } CATCH_LOG(); @@ -1009,12 +1008,15 @@ namespace winrt::TerminalApp::implementation if (FAILED(_settingsLoadedResult)) { - _LoadErrorsDialogRoutine(); + const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle"); + const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText"); + _ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult); return; } - else if (_settingsLoadedResult == S_FALSE) + + if (_settingsLoadedResult == S_FALSE) { - _ShowLoadWarningsDialogRoutine(); + _ShowLoadWarningsDialog(); } // Here, we successfully reloaded the settings, and created a new @@ -1023,6 +1025,7 @@ namespace winrt::TerminalApp::implementation // Update the settings in TerminalPage _root->SetSettings(_settings, true); + _ApplyLanguageSettingChange(); _RefreshThemeRoutine(); _ApplyStartupTaskStateChange(); diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 50b7b95f0..bb02b7298 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -124,18 +124,15 @@ namespace winrt::TerminalApp::implementation ::TerminalApp::AppCommandlineArgs _appArgs; ::TerminalApp::AppCommandlineArgs _settingsAppArgs; - int _ParseArgs(winrt::array_view& args); static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view args, const Microsoft::Terminal::Settings::Model::WindowingMode& windowingBehavior); void _ShowLoadErrorsDialog(const winrt::hstring& titleKey, const winrt::hstring& contentKey, HRESULT settingsLoadedResult); void _ShowLoadWarningsDialog(); bool _IsKeyboardServiceEnabled(); - void _ShowKeyboardServiceDisabledDialog(); - fire_and_forget _LoadErrorsDialogRoutine(); - fire_and_forget _ShowLoadWarningsDialogRoutine(); - fire_and_forget _RefreshThemeRoutine(); + void _ApplyLanguageSettingChange() noexcept; + void _RefreshThemeRoutine(); fire_and_forget _ApplyStartupTaskStateChange(); void _OnLoaded(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); diff --git a/src/cascadia/TerminalApp/ColorPickupFlyout.cpp b/src/cascadia/TerminalApp/ColorPickupFlyout.cpp index b1c15be48..ed1d20b83 100644 --- a/src/cascadia/TerminalApp/ColorPickupFlyout.cpp +++ b/src/cascadia/TerminalApp/ColorPickupFlyout.cpp @@ -96,7 +96,7 @@ namespace winrt::TerminalApp::implementation Hide(); } - void ColorPickupFlyout::ColorPicker_ColorChanged(const Windows::UI::Xaml::Controls::ColorPicker&, const Windows::UI::Xaml::Controls::ColorChangedEventArgs& args) + void ColorPickupFlyout::ColorPicker_ColorChanged(const Microsoft::UI::Xaml::Controls::ColorPicker&, const Microsoft::UI::Xaml::Controls::ColorChangedEventArgs& args) { _ColorSelectedHandlers(args.NewColor()); } diff --git a/src/cascadia/TerminalApp/ColorPickupFlyout.h b/src/cascadia/TerminalApp/ColorPickupFlyout.h index c73f68881..b124dfe43 100644 --- a/src/cascadia/TerminalApp/ColorPickupFlyout.h +++ b/src/cascadia/TerminalApp/ColorPickupFlyout.h @@ -12,7 +12,7 @@ namespace winrt::TerminalApp::implementation void ShowColorPickerButton_Click(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& args); void CustomColorButton_Click(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& args); void ClearColorButton_Click(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& args); - void ColorPicker_ColorChanged(const Windows::UI::Xaml::Controls::ColorPicker&, const Windows::UI::Xaml::Controls::ColorChangedEventArgs& args); + void ColorPicker_ColorChanged(const Microsoft::UI::Xaml::Controls::ColorPicker&, const Microsoft::UI::Xaml::Controls::ColorChangedEventArgs& args); WINRT_CALLBACK(ColorCleared, TerminalApp::ColorClearedArgs); WINRT_CALLBACK(ColorSelected, TerminalApp::ColorSelectedArgs); diff --git a/src/cascadia/TerminalApp/ColorPickupFlyout.xaml b/src/cascadia/TerminalApp/ColorPickupFlyout.xaml index 7674b357e..b7da875af 100644 --- a/src/cascadia/TerminalApp/ColorPickupFlyout.xaml +++ b/src/cascadia/TerminalApp/ColorPickupFlyout.xaml @@ -4,190 +4,200 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:TerminalApp" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Converters.idl b/src/cascadia/TerminalSettingsEditor/Converters.idl index 58fb27db0..89eb05791 100644 --- a/src/cascadia/TerminalSettingsEditor/Converters.idl +++ b/src/cascadia/TerminalSettingsEditor/Converters.idl @@ -39,6 +39,11 @@ namespace Microsoft.Terminal.Settings.Editor PercentageConverter(); }; + runtimeclass PercentageSignConverter : [default] Windows.UI.Xaml.Data.IValueConverter + { + PercentageSignConverter(); + }; + runtimeclass StringIsEmptyConverter : [default] Windows.UI.Xaml.Data.IValueConverter { StringIsEmptyConverter(); diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp index 7556dc067..d45c34e13 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp @@ -2,10 +2,13 @@ // Licensed under the MIT license. #include "pch.h" +#include "EnumEntry.h" #include "GlobalAppearance.h" #include "GlobalAppearance.g.cpp" #include "GlobalAppearancePageNavigationState.g.cpp" -#include "EnumEntry.h" + +#include +#include using namespace winrt; using namespace winrt::Windows::UI::Xaml; @@ -16,6 +19,11 @@ using namespace winrt::Windows::Foundation::Collections; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { + // For ComboBox an empty SelectedItem string denotes no selection. + // What we want instead is for "Use system language" to be selected by default. + // --> "und" is synonymous for "Use system language". + constexpr std::wstring_view systemLanguageTag{ L"und" }; + GlobalAppearance::GlobalAppearance() { InitializeComponent(); @@ -28,4 +36,166 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _State = e.Parameter().as(); } + + winrt::hstring GlobalAppearance::LanguageDisplayConverter(const winrt::hstring& tag) + { + if (tag == systemLanguageTag) + { + return RS_(L"Globals_LanguageDefault"); + } + + winrt::Windows::Globalization::Language language{ tag }; + return language.NativeName(); + } + + // Returns whether the language selector is available/shown. + // + // winrt::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride() + // doesn't work for unpackaged applications. The corresponding code in TerminalApp is disabled. + // It would be confusing for our users if we presented a dysfunctional language selector. + bool GlobalAppearance::LanguageSelectorAvailable() + { + return IsPackaged(); + } + + // Returns the list of languages the user may override the application language with. + // The returned list are BCP 47 language tags like {"und", "en-US", "de-DE", "es-ES", ...}. + // "und" is short for "undefined" and is synonymous for "Use system language" in this code. + winrt::Windows::Foundation::Collections::IObservableVector GlobalAppearance::LanguageList() + { + if (_languageList) + { + return _languageList; + } + + if (!LanguageSelectorAvailable()) + { + _languageList = {}; + return _languageList; + } + + // In order to return the language list this code does the following: + // [1] Get all possible languages we want to allow the user to choose. + // We have to acquire languages from multiple sources, creating duplicates. See below at [1]. + // [2] Sort languages by their ASCII tags, forcing the UI in a consistent/stable order. + // I wanted to sort the localized language names initially, but it turned out to be complex. + // [3] Remove potential duplicates in our language list from [1]. + // We don't want to have en-US twice in the list, do we? + // [4] Optionally remove unwanted language tags (like pseudo-localizations). + + std::vector tags; + + // [1]: + { + // ManifestLanguages contains languages the app ships with. + // + // Languages is a computed list that merges the ManifestLanguages with the + // user's ranked list of preferred languages taken from the system settings. + // As is tradition the API documentation is incomplete though, as it can also + // contain regional language variants. If our app supports en-US, but the user + // has en-GB or en-DE in their system's preferred language list, Languages will + // contain those as well, as they're variants from a supported language. We should + // allow a user to select those, as regional formattings can vary significantly. + const std::array tagSources{ + winrt::Windows::Globalization::ApplicationLanguages::ManifestLanguages(), + winrt::Windows::Globalization::ApplicationLanguages::Languages() + }; + + // tags will hold all the flattened results from tagSources. + // We resize() the vector to the proper size in order to efficiently GetMany() all items. + tags.resize(std::accumulate( + tagSources.begin(), + tagSources.end(), + // tags[0] will be "und" - the "Use system language" item + // tags[1..n] will contain tags from tagSources. + // --> totalTags is offset by 1 + 1, + [](uint32_t sum, const auto& v) -> uint32_t { + return sum + v.Size(); + })); + + // As per the function definition, the first item + // is always "Use system language" ("und"). + auto data = tags.data(); + *data++ = systemLanguageTag; + + // Finally GetMany() all the tags from tagSources. + for (const auto& v : tagSources) + { + const auto size = v.Size(); + v.GetMany(0, winrt::array_view(data, size)); + data += size; + } + } + + // NOTE: The size of tags is always >0, due to tags[0] being hardcoded to "und". + const auto tagsBegin = ++tags.begin(); + const auto tagsEnd = tags.end(); + + // [2]: + std::sort(tagsBegin, tagsEnd); + + // I'd love for both, std::unique and std::remove_if, to occur in a single loop, + // but the code turned out to be complex and even less maintainable, so I gave up. + { + // [3] part 1: + auto it = std::unique(tagsBegin, tagsEnd); + + // The qps- languages are useful for testing ("pseudo-localization"). + // --> Leave them in if debug features are enabled. + if (!_State.Globals().DebugFeaturesEnabled()) + { + // [4] part 1: + it = std::remove_if(tagsBegin, it, [](const winrt::hstring& tag) -> bool { + return til::starts_with(tag, L"qps-"); + }); + } + + // [3], [4] part 2 (completing the so called "erase-remove idiom"): + tags.erase(it, tagsEnd); + } + + _languageList = winrt::single_threaded_observable_vector(std::move(tags)); + return _languageList; + } + + winrt::Windows::Foundation::IInspectable GlobalAppearance::CurrentLanguage() + { + if (_currentLanguage) + { + return _currentLanguage; + } + + if (!LanguageSelectorAvailable()) + { + _currentLanguage = {}; + return _currentLanguage; + } + + // NOTE: PrimaryLanguageOverride throws if this instance is unpackaged. + auto currentLanguage = winrt::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride(); + if (currentLanguage.empty()) + { + currentLanguage = systemLanguageTag; + } + + _currentLanguage = winrt::box_value(currentLanguage); + return _currentLanguage; + } + + void GlobalAppearance::CurrentLanguage(const winrt::Windows::Foundation::IInspectable& tag) + { + _currentLanguage = tag; + + const auto currentLanguage = winrt::unbox_value(_currentLanguage); + const auto globals = _State.Globals(); + if (currentLanguage == systemLanguageTag) + { + globals.ClearLanguage(); + } + else + { + globals.Language(currentLanguage); + } + } } diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h index dddbf23f5..c46053953 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h @@ -26,9 +26,23 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); WINRT_PROPERTY(Editor::GlobalAppearancePageNavigationState, State, nullptr); - GETSET_BINDABLE_ENUM_SETTING(Theme, winrt::Windows::UI::Xaml::ElementTheme, State().Globals, Theme); GETSET_BINDABLE_ENUM_SETTING(TabWidthMode, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, State().Globals, TabWidthMode); + + public: + // LanguageDisplayConverter maps the given BCP 47 tag to a localized string. + // For instance "en-US" produces "English (United States)", while "de-DE" produces + // "Deutsch (Deutschland)". This works independently of the user's locale. + static winrt::hstring LanguageDisplayConverter(const winrt::hstring& tag); + + bool LanguageSelectorAvailable(); + winrt::Windows::Foundation::Collections::IObservableVector LanguageList(); + winrt::Windows::Foundation::IInspectable CurrentLanguage(); + void CurrentLanguage(const winrt::Windows::Foundation::IInspectable& tag); + + private: + winrt::Windows::Foundation::Collections::IObservableVector _languageList; + winrt::Windows::Foundation::IInspectable _currentLanguage; }; } diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl index 294f73022..7e3483140 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "EnumEntry.idl"; +import "EnumEntry.idl"; namespace Microsoft.Terminal.Settings.Editor { @@ -15,6 +15,11 @@ namespace Microsoft.Terminal.Settings.Editor GlobalAppearance(); GlobalAppearancePageNavigationState State { get; }; + static String LanguageDisplayConverter(String tag); + Boolean LanguageSelectorAvailable { get; }; + Windows.Foundation.Collections.IObservableVector LanguageList { get; }; + IInspectable CurrentLanguage; + IInspectable CurrentTheme; Windows.Foundation.Collections.IObservableVector ThemeList { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml index 84c1408ae..ae030da7c 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml @@ -28,9 +28,22 @@ + + + + + + + + + + + - + diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index fc256a9f4..69d52294e 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -65,13 +65,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // - settings - the new settings source // Return value: // - - fire_and_forget MainPage::UpdateSettings(Model::CascadiaSettings settings) + void MainPage::UpdateSettings(const Model::CascadiaSettings& settings) { _settingsSource = settings; _settingsClone = settings.Copy(); - co_await winrt::resume_foreground(Dispatcher()); - // Deduce information about the currently selected item IInspectable selectedItemTag; auto menuItems{ SettingsNav().MenuItems() }; @@ -83,34 +81,43 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } - // remove all profile-related NavViewItems by populating a std::vector - // with the ones we want to keep. - // NOTE: menuItems.Remove() causes an out-of-bounds crash. Using ReplaceAll() - // gets around this crash. - std::vector menuItemsSTL; - for (const auto& item : menuItems) - { - if (const auto& navViewItem{ item.try_as() }) - { - if (const auto& tag{ navViewItem.Tag() }) - { - if (tag.try_as()) + // We'll remove a bunch of items and iterate over it twice. + // --> Copy it into an STL vector to simplify our code and reduce COM overhead. + std::vector menuItemsSTL(menuItems.Size(), nullptr); + menuItems.GetMany(0, menuItemsSTL); + + // We want to refresh the list of profiles in the NavigationView. + // In order to add profiles we can use _InitializeProfilesList(); + // But before we can do that we have to remove existing profiles first of course. + // This "erase-remove" idiom will achieve just that. + menuItemsSTL.erase( + std::remove_if( + menuItemsSTL.begin(), + menuItemsSTL.end(), + [](const auto& item) -> bool { + if (const auto& navViewItem{ item.try_as() }) { - // don't add NavViewItem pointing to a Profile - continue; - } - else if (const auto& stringTag{ tag.try_as() }) - { - if (stringTag == addProfileTag) + if (const auto& tag{ navViewItem.Tag() }) { - // don't add the "Add Profile" item - continue; + if (tag.try_as()) + { + // remove NavViewItem pointing to a Profile + return true; + } + if (const auto& stringTag{ tag.try_as() }) + { + if (stringTag == addProfileTag) + { + // remove the "Add Profile" item + return true; + } + } } } - } - } - menuItemsSTL.emplace_back(item); - } + return false; + }), + menuItemsSTL.end()); + menuItems.ReplaceAll(menuItemsSTL); // Repopulate profile-related menu items @@ -123,7 +130,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // refresh the current page using the SelectedItem data we collected before the refresh if (selectedItemTag) { - for (const auto& item : menuItems) + for (const auto& item : menuItemsSTL) { if (const auto& menuItem{ item.try_as() }) { @@ -138,7 +145,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // found the one that was selected before the refresh SettingsNav().SelectedItem(item); _Navigate(*stringTag); - co_return; + return; } } } @@ -151,7 +158,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // found the one that was selected before the refresh SettingsNav().SelectedItem(item); _Navigate(*profileTag); - co_return; + return; } } } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index 677b7c6d1..401c2576e 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -13,7 +13,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation MainPage() = delete; MainPage(const Model::CascadiaSettings& settings); - fire_and_forget UpdateSettings(Model::CascadiaSettings settings); + void UpdateSettings(const Model::CascadiaSettings& settings); void OpenJsonKeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& args); void OpenJsonTapped(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::TappedRoutedEventArgs const& args); diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj index de8f8435b..a3df59932 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj @@ -60,6 +60,9 @@ Converters.idl + + Converters.idl + Converters.idl @@ -167,6 +170,9 @@ Converters.idl + + Converters.idl + Converters.idl @@ -332,4 +338,4 @@ - + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters index ba44e4844..4273f715d 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters @@ -10,10 +10,16 @@ + + Converters + + + Converters + @@ -43,4 +49,4 @@ {00f725c8-41b4-40a8-995e-8ee2e49a4a4c} - + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsEditor/PercentageSignConverter.cpp b/src/cascadia/TerminalSettingsEditor/PercentageSignConverter.cpp new file mode 100644 index 000000000..a8acb84d1 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/PercentageSignConverter.cpp @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "PercentageSignConverter.h" +#include "PercentageSignConverter.g.cpp" + +using namespace winrt::Windows; +using namespace winrt::Windows::UI::Xaml; + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + Foundation::IInspectable PercentageSignConverter::Convert(Foundation::IInspectable const& value, + Windows::UI::Xaml::Interop::TypeName const& /* targetType */, + Foundation::IInspectable const& /* parameter */, + hstring const& /* language */) + { + const auto number{ winrt::unbox_value(value) }; + return winrt::box_value(to_hstring((int)number) + L"%"); + } + + Foundation::IInspectable PercentageSignConverter::ConvertBack(Foundation::IInspectable const& /*value*/, + Windows::UI::Xaml::Interop::TypeName const& /* targetType */, + Foundation::IInspectable const& /*parameter*/, + hstring const& /* language */) + { + throw hresult_not_implemented(); + } +} diff --git a/src/cascadia/TerminalSettingsEditor/PercentageSignConverter.h b/src/cascadia/TerminalSettingsEditor/PercentageSignConverter.h new file mode 100644 index 000000000..08b4f13b3 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/PercentageSignConverter.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "PercentageSignConverter.g.h" +#include "Utils.h" + +DECLARE_CONVERTER(winrt::Microsoft::Terminal::Settings::Editor, PercentageSignConverter); diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.xaml b/src/cascadia/TerminalSettingsEditor/Profiles.xaml index 68c592d5b..3ec1ab719 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles.xaml @@ -35,6 +35,7 @@ + @@ -626,7 +627,7 @@ Value="{x:Bind State.Profile.BackgroundImageOpacity, Converter={StaticResource PercentageConverter}, Mode=TwoWay}" /> + Text="{Binding ElementName=BIOpacitySlider, Path=Value, Mode=OneWay, Converter={StaticResource PercentageSignConverter}}" /> @@ -664,7 +665,7 @@ Value="{x:Bind State.Profile.AcrylicOpacity, Converter={StaticResource PercentageConverter}, Mode=TwoWay}" /> + Text="{Binding ElementName=AcrylicOpacitySlider, Path=Value, Mode=OneWay, Converter={StaticResource PercentageSignConverter}}" /> diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index a42629596..495191792 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -215,6 +215,18 @@ Yellow This is the header for a control that lets the user select the yellow color for text displayed on the screen. + + Language + The header for a control allowing users to choose the app's language. + + + Sets an override for the app's preferred language. + A description explaining how this control changes the app's language. + + + Use system default + The app contains a control allowing users to choose the app's language. If this value is chosen, the language preference list of the system settings is used instead. + Always show tabs Header for a control to toggle if the app should always show the tabs (similar to a website browser). diff --git a/src/cascadia/TerminalSettingsEditor/pch.h b/src/cascadia/TerminalSettingsEditor/pch.h index df45f6bd2..49065c556 100644 --- a/src/cascadia/TerminalSettingsEditor/pch.h +++ b/src/cascadia/TerminalSettingsEditor/pch.h @@ -20,21 +20,15 @@ #undef GetCurrentTime #endif -#include - -#include - -#include +#include #include #include -#include -#include - +#include #include #include -#include #include #include +#include #include #include #include diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 6cf7d8cb5..280ac9ac1 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -68,6 +68,15 @@ constexpr size_t Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(c return gsl::narrow_cast(args.Hash()); } +// Retrieves the hash value for an empty-constructed object. +template +static size_t EmptyHash() +{ + // cache the value of the empty hash + static const size_t cachedHash = winrt::make_self()->Hash(); + return cachedHash; +} + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { using namespace ::Microsoft::Terminal::Settings::Model; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 433535dc5..5decbab3d 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT license. #include "pch.h" +#include "AllShortcutActions.h" #include "ActionMap.h" #include "ActionMap.g.cpp" @@ -18,18 +19,36 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation size_t hashedArgs{}; if (const auto& args{ actionAndArgs.Args() }) { + // Args are defined, so hash them hashedArgs = gsl::narrow_cast(args.Hash()); } else { - std::hash argsHash; - hashedArgs = argsHash(nullptr); + // Args are not defined. + // Check if the ShortcutAction supports args. + switch (actionAndArgs.Action()) + { +#define ON_ALL_ACTIONS_WITH_ARGS(action) \ + case ShortcutAction::action: \ + /* If it does, hash the default values for the args.*/ \ + hashedArgs = EmptyHash(); \ + break; + ALL_SHORTCUT_ACTIONS_WITH_ARGS +#undef ON_ALL_ACTIONS_WITH_ARGS + default: + { + // Otherwise, hash nullptr. + std::hash argsHash; + hashedArgs = argsHash(nullptr); + } + } } return hashedAction ^ hashedArgs; } ActionMap::ActionMap() : - _NestedCommands{ single_threaded_map() } + _NestedCommands{ single_threaded_map() }, + _IterableCommands{ single_threaded_vector() } { } @@ -87,7 +106,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { // populate _NameMapCache std::unordered_map nameMap{}; - _PopulateNameMapWithNestedCommands(nameMap); + _PopulateNameMapWithSpecialCommands(nameMap); _PopulateNameMapWithStandardCommands(nameMap); _NameMapCache = single_threaded_map(std::move(nameMap)); @@ -96,18 +115,19 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } // Method Description: - // - Populates the provided nameMap with all of our nested commands and our parents nested commands - // - Performs a top-down approach by going to the root first, then recursively adding the nested commands layer-by-layer + // - Populates the provided nameMap with all of our special commands and our parent's special commands. + // - Special commands include nested and iterable commands. + // - Performs a top-down approach by going to the root first, then recursively adding the nested commands layer-by-layer. // Arguments: // - nameMap: the nameMap we're populating. This maps the name (hstring) of a command to the command itself. - void ActionMap::_PopulateNameMapWithNestedCommands(std::unordered_map& nameMap) const + void ActionMap::_PopulateNameMapWithSpecialCommands(std::unordered_map& nameMap) const { // Update NameMap with our parents. // Starting with this means we're doing a top-down approach. FAIL_FAST_IF(_parents.size() > 1); for (const auto& parent : _parents) { - parent->_PopulateNameMapWithNestedCommands(nameMap); + parent->_PopulateNameMapWithSpecialCommands(nameMap); } // Add NestedCommands to NameMap _after_ we handle our parents. @@ -125,6 +145,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation nameMap.erase(name); } } + + // Add IterableCommands to NameMap + for (const auto& cmd : _IterableCommands) + { + nameMap.insert_or_assign(cmd.Name(), cmd); + } } // Method Description: @@ -296,6 +322,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation actionMap->_NestedCommands.Insert(name, *(get_self(cmd)->Copy())); } + // copy _IterableCommands + for (const auto& cmd : _IterableCommands) + { + actionMap->_IterableCommands.Append(*(get_self(cmd)->Copy())); + } + // repeat this for each of our parents FAIL_FAST_IF(_parents.size() > 1); for (const auto& parent : _parents) @@ -336,6 +368,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return; } + // Handle iterable commands + if (cmdImpl->IterateOn() != ExpandCommandType::None) + { + _IterableCommands.Append(cmd); + return; + } + // General Case: // Add the new command to the KeyMap. // This map directs you to an entry in the ActionMap. diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index e33638cff..3435d120c 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -81,7 +81,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::optional _GetActionByID(const InternalActionID actionID) const; std::optional _GetActionByKeyChordInternal(Control::KeyChord const& keys) const; - void _PopulateNameMapWithNestedCommands(std::unordered_map& nameMap) const; + void _PopulateNameMapWithSpecialCommands(std::unordered_map& nameMap) const; void _PopulateNameMapWithStandardCommands(std::unordered_map& nameMap) const; void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap, std::unordered_set& unboundKeys) const; std::vector _GetCumulativeActions() const noexcept; @@ -94,6 +94,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMap _GlobalHotkeysCache{ nullptr }; Windows::Foundation::Collections::IMap _KeyBindingMapCache{ nullptr }; Windows::Foundation::Collections::IMap _NestedCommands{ nullptr }; + Windows::Foundation::Collections::IVector _IterableCommands{ nullptr }; std::unordered_map _KeyMap; std::unordered_map _ActionMap; diff --git a/src/cascadia/TerminalSettingsModel/ActionMapSerialization.cpp b/src/cascadia/TerminalSettingsModel/ActionMapSerialization.cpp index 1358c7865..5956bf757 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMapSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMapSerialization.cpp @@ -60,34 +60,40 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { Json::Value actionList{ Json::ValueType::arrayValue }; - // Serialize all standard Command objects in the current layer - for (const auto& [_, cmd] : _ActionMap) - { - // Command serializes to an array of JSON objects. - // This is because a Command may have multiple key chords associated with it. - // The name and icon are only serialized in the first object. - // Example: - // { "name": "Custom Copy", "command": "copy", "keys": "ctrl+c" } - // { "command": "copy", "keys": "ctrl+shift+c" } - // { "command": "copy", "keys": "ctrl+ins" } + // Command serializes to an array of JSON objects. + // This is because a Command may have multiple key chords associated with it. + // The name and icon are only serialized in the first object. + // Example: + // { "name": "Custom Copy", "command": "copy", "keys": "ctrl+c" } + // { "command": "copy", "keys": "ctrl+shift+c" } + // { "command": "copy", "keys": "ctrl+ins" } + auto toJson = [&actionList](const Model::Command& cmd) { const auto cmdImpl{ winrt::get_self(cmd) }; const auto& cmdJsonArray{ cmdImpl->ToJson() }; for (const auto& cmdJson : cmdJsonArray) { actionList.append(cmdJson); } + }; + + // Serialize all standard Command objects in the current layer + for (const auto& [_, cmd] : _ActionMap) + { + toJson(cmd); } // Serialize all nested Command objects added in the current layer for (const auto& [_, cmd] : _NestedCommands) { - const auto cmdImpl{ winrt::get_self(cmd) }; - const auto& cmdJsonArray{ cmdImpl->ToJson() }; - for (const auto& cmdJson : cmdJsonArray) - { - actionList.append(cmdJson); - } + toJson(cmd); } + + // Serialize all iterable Command objects added in the current layer + for (const auto& cmd : _IterableCommands) + { + toJson(cmd); + } + return actionList; } diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h index d0b08e9e2..cdcdc9613 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h @@ -16,7 +16,6 @@ Author(s): #pragma once -#include "pch.h" #include "AppearanceConfig.g.h" #include "JsonUtils.h" #include "../inc/cppwinrt_utils.h" diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 5e45159e7..a6df3c0e1 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -2,21 +2,14 @@ // Licensed under the MIT license. #include "pch.h" -#include -#include -#include -#include #include "CascadiaSettings.h" -#include "../../types/inc/utils.hpp" -#include "../../inc/DefaultSettings.h" -#include "Utils.h" -#include "LibraryResources.h" +#include "CascadiaSettings.g.cpp" +#include + +#include "AzureCloudShellGenerator.h" #include "PowershellCoreProfileGenerator.h" #include "WslDistroGenerator.h" -#include "AzureCloudShellGenerator.h" - -#include "CascadiaSettings.g.cpp" using namespace ::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 04b5e6650..a4e328ebe 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -147,7 +147,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::unordered_set _AccumulateJsonFilesInDirectory(const std::wstring_view directory); void _ParseAndLayerFragmentFiles(const std::unordered_set files, const winrt::hstring source); - static bool _IsPackaged(); static void _WriteSettings(std::string_view content, const hstring filepath); static std::optional _ReadUserSettings(); static std::optional _ReadFile(HANDLE hFile); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 9867117fc..0f6ad7593 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -2,15 +2,12 @@ // Licensed under the MIT license. #include "pch.h" -#include #include "CascadiaSettings.h" -#include "../../types/inc/utils.hpp" -#include "Utils.h" -#include "JsonUtils.h" -#include -#include + #include -#include "DefaultProfileUtils.h" +#include + +#include // defaults.h is a file containing the default json settings in a std::string_view #include "defaults.h" @@ -1058,20 +1055,6 @@ winrt::com_ptr CascadiaSettings::_FindMatchingColorScheme(const Jso return nullptr; } -// Function Description: -// - Returns true if we're running in a packaged context. -// If we are, we want to change our settings path slightly. -// Arguments: -// - -// Return Value: -// - true iff we're running in a packaged context. -bool CascadiaSettings::_IsPackaged() -{ - UINT32 length = 0; - LONG rc = GetCurrentPackageFullName(&length, nullptr); - return rc != APPMODEL_ERROR_NO_PACKAGE; -} - // 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. @@ -1216,7 +1199,7 @@ winrt::hstring CascadiaSettings::SettingsPath() std::filesystem::path parentDirectoryForSettingsFile{ localAppDataFolder.get() }; - if (!_IsPackaged()) + if (!IsPackaged()) { parentDirectoryForSettingsFile /= UnpackagedSettingsFolderName; } diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index ee560b78d..dbdfd6e5c 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -394,10 +394,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { Json::Value cmdList{ Json::ValueType::arrayValue }; - if (_nestedCommand) + if (_nestedCommand || _IterateOn != ExpandCommandType::None) { - // handle nested command - // For nested commands, we can trust _originalJson to be correct. + // handle special commands + // For these, we can trust _originalJson to be correct. // In fact, we _need_ to use it here because we don't actually deserialize `iterateOn` // until we expand the command. cmdList.append(_originalJson); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 39c370228..466031b05 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -26,6 +26,7 @@ static constexpr std::string_view InitialColsKey{ "initialCols" }; static constexpr std::string_view InitialPositionKey{ "initialPosition" }; static constexpr std::string_view CenterOnLaunchKey{ "centerOnLaunch" }; static constexpr std::string_view ShowTitleInTitlebarKey{ "showTerminalTitleInTitlebar" }; +static constexpr std::string_view LanguageKey{ "language" }; static constexpr std::string_view ThemeKey{ "theme" }; static constexpr std::string_view TabWidthModeKey{ "tabWidthMode" }; static constexpr std::string_view ShowTabsInTitlebarKey{ "showTabsInTitlebar" }; @@ -101,6 +102,7 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_AlwaysShowTabs = _AlwaysShowTabs; globals->_ShowTitleInTitlebar = _ShowTitleInTitlebar; globals->_ConfirmCloseAllTabs = _ConfirmCloseAllTabs; + globals->_Language = _Language; globals->_Theme = _Theme; globals->_TabWidthMode = _TabWidthMode; globals->_ShowTabsInTitlebar = _ShowTabsInTitlebar; @@ -279,6 +281,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, LaunchModeKey, _LaunchMode); + JsonUtils::GetValueForKey(json, LanguageKey, _Language); + JsonUtils::GetValueForKey(json, ThemeKey, _Theme); JsonUtils::GetValueForKey(json, TabWidthModeKey, _TabWidthMode); @@ -393,6 +397,7 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, WarnAboutLargePasteKey, _WarnAboutLargePaste); JsonUtils::SetValueForKey(json, WarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste); JsonUtils::SetValueForKey(json, LaunchModeKey, _LaunchMode); + JsonUtils::SetValueForKey(json, LanguageKey, _Language); JsonUtils::SetValueForKey(json, ThemeKey, _Theme); JsonUtils::SetValueForKey(json, TabWidthModeKey, _TabWidthMode); JsonUtils::SetValueForKey(json, SnapToGridOnResizeKey, _SnapToGridOnResize); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index f93bcd0e3..e11807b2a 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -65,6 +65,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowTabs, true); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ShowTitleInTitlebar, true); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ConfirmCloseAllTabs, true); + INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, Language); INHERITABLE_SETTING(Model::GlobalAppSettings, winrt::Windows::UI::Xaml::ElementTheme, Theme, winrt::Windows::UI::Xaml::ElementTheme::Default); INHERITABLE_SETTING(Model::GlobalAppSettings, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ShowTabsInTitlebar, true); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 9f3c8c745..fba59e1a4 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -43,6 +43,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, AlwaysShowTabs); INHERITABLE_SETTING(Boolean, ShowTitleInTitlebar); INHERITABLE_SETTING(Boolean, ConfirmCloseAllTabs); + INHERITABLE_SETTING(String, Language); INHERITABLE_SETTING(Windows.UI.Xaml.ElementTheme, Theme); INHERITABLE_SETTING(Microsoft.UI.Xaml.Controls.TabViewWidthMode, TabWidthMode); INHERITABLE_SETTING(Boolean, ShowTabsInTitlebar); diff --git a/src/cascadia/TerminalSettingsModel/pch.h b/src/cascadia/TerminalSettingsModel/pch.h index 688719cd7..d5473939c 100644 --- a/src/cascadia/TerminalSettingsModel/pch.h +++ b/src/cascadia/TerminalSettingsModel/pch.h @@ -25,23 +25,22 @@ #include -#include - -#include - -#include #include -#include +#include #include +#include +#include +#include #include #include #include -#include - -#include #include +#include +#include +#include + // Including TraceLogging essentials for the binary #include #include @@ -52,11 +51,5 @@ TRACELOGGING_DECLARE_PROVIDER(g_hSettingsModelProvider); // JsonCpp #include -#include - -#include -#include -#include - // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" diff --git a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj index 6f088a269..be5cee9e0 100644 --- a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj +++ b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj @@ -17,6 +17,7 @@ + diff --git a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj.filters b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj.filters index 670ca3581..edaac5a3d 100644 --- a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj.filters +++ b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj.filters @@ -12,6 +12,7 @@ + diff --git a/src/cascadia/WinRTUtils/inc/ThrottledFunc.h b/src/cascadia/WinRTUtils/inc/ThrottledFunc.h new file mode 100644 index 000000000..d5ff0a0e4 --- /dev/null +++ b/src/cascadia/WinRTUtils/inc/ThrottledFunc.h @@ -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 +class ThrottledFunc : public std::enable_shared_from_this> +{ +public: + using filetime_duration = std::chrono::duration>; + using function = std::function; + + // 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 + void Run(MakeArgs&&... args) + { + if (!_storage.emplace(std::forward(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 + 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(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 _storage; +}; + +template +using ThrottledFuncTrailing = ThrottledFunc; +using ThrottledFuncLeading = ThrottledFunc; diff --git a/src/cascadia/WinRTUtils/inc/WtExeUtils.h b/src/cascadia/WinRTUtils/inc/WtExeUtils.h index 1fa7a5708..aed728db5 100644 --- a/src/cascadia/WinRTUtils/inc/WtExeUtils.h +++ b/src/cascadia/WinRTUtils/inc/WtExeUtils.h @@ -1,9 +1,23 @@ +constexpr std::wstring_view WtExe{ L"wt.exe" }; +constexpr std::wstring_view WtdExe{ L"wtd.exe" }; +constexpr std::wstring_view WindowsTerminalExe{ L"WindowsTerminal.exe" }; +constexpr std::wstring_view LocalAppDataAppsPath{ L"%LOCALAPPDATA%\\Microsoft\\WindowsApps\\" }; -static constexpr std::wstring_view WtExe{ L"wt.exe" }; -static constexpr std::wstring_view WtdExe{ L"wtd.exe" }; -static constexpr std::wstring_view WindowsTerminalExe{ L"WindowsTerminal.exe" }; - -static constexpr std::wstring_view LocalAppDataAppsPath{ L"%LOCALAPPDATA%\\Microsoft\\WindowsApps\\" }; +_TIL_INLINEPREFIX bool IsPackaged() +{ + static const bool isPackaged = []() -> bool { + try + { + const auto package = winrt::Windows::ApplicationModel::Package::Current(); + return true; + } + catch (...) + { + return false; + } + }(); + return isPackaged; +} // Function Description: // - This is a helper to determine if we're running as a part of the Dev Build @@ -18,19 +32,21 @@ static constexpr std::wstring_view LocalAppDataAppsPath{ L"%LOCALAPPDATA%\\Micro _TIL_INLINEPREFIX bool IsDevBuild() { // use C++11 magic statics to make sure we only do this once. - static bool isDevBuild = []() -> bool { - try + static const bool isDevBuild = []() -> bool { + if (IsPackaged()) { - const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; - const auto id = package.Id(); - const std::wstring name{ id.FullName() }; - // Does our PFN start with WindowsTerminalDev? - return name.rfind(L"WindowsTerminalDev", 0) == 0; + try + { + const auto package = winrt::Windows::ApplicationModel::Package::Current(); + const auto id = package.Id(); + const auto name = id.FullName(); + return til::starts_with(name, L"WindowsTerminalDev"); + } + CATCH_LOG(); } - CATCH_LOG(); + return true; }(); - return isDevBuild; } @@ -45,9 +61,8 @@ _TIL_INLINEPREFIX bool IsDevBuild() // - // Return Value: // - the full path to the exe, one of `wt.exe`, `wtd.exe`, or `WindowsTerminal.exe`. -_TIL_INLINEPREFIX std::wstring GetWtExePath() +_TIL_INLINEPREFIX const std::wstring& GetWtExePath() { - // use C++11 magic statics to make sure we only do this once. static const std::wstring exePath = []() -> std::wstring { // First, check a packaged location for the exe. If we've got a package // family name, that means we're one of the packaged Dev build, packaged @@ -57,37 +72,35 @@ _TIL_INLINEPREFIX std::wstring GetWtExePath() // `wt.exe` on the %PATH% is us or not. Fortunately, _our_ execution alias // is located in "%LOCALAPPDATA%\Microsoft\WindowsApps\", _always_, so we can use that to look up the exe easier. - try + if (IsPackaged()) { - const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; - const auto id = package.Id(); - const std::wstring pfn{ id.FamilyName() }; - if (!pfn.empty()) + try { - const std::filesystem::path windowsAppsPath{ wil::ExpandEnvironmentStringsW(LocalAppDataAppsPath.data()) }; - const std::filesystem::path wtPath = windowsAppsPath / pfn / (IsDevBuild() ? WtdExe : WtExe); - return wtPath; + const auto package = winrt::Windows::ApplicationModel::Package::Current(); + const auto id = package.Id(); + const auto pfn = id.FamilyName(); + if (!pfn.empty()) + { + const std::filesystem::path windowsAppsPath{ wil::ExpandEnvironmentStringsW(LocalAppDataAppsPath.data()) }; + const std::filesystem::path wtPath = windowsAppsPath / std::wstring_view{ pfn } / (IsDevBuild() ? WtdExe : WtExe); + return wtPath; + } } + CATCH_LOG(); } - CATCH_LOG(); // If we're here, then we couldn't resolve our exe from the package. This // means we're running unpackaged. We should just use the // WindowsTerminal.exe that's sitting in the directory next to us. try { - HMODULE hModule = GetModuleHandle(nullptr); - THROW_LAST_ERROR_IF(hModule == nullptr); - std::wstring dllPathString; - THROW_IF_FAILED(wil::GetModuleFileNameW(hModule, dllPathString)); - const std::filesystem::path dllPath{ dllPathString }; - const std::filesystem::path rootDir = dllPath.parent_path(); - std::filesystem::path wtPath = rootDir / WindowsTerminalExe; - return wtPath; + std::filesystem::path module = wil::GetModuleFileNameW(nullptr); + module.replace_filename(WindowsTerminalExe); + return module; } CATCH_LOG(); - return L"wt.exe"; + return std::wstring{ WtExe }; }(); return exePath; } diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 00fe42c04..753166bf1 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -594,6 +594,7 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send summonArgs.MoveToCurrentDesktop(false); summonArgs.DropdownDuration(0); summonArgs.ToMonitor(Remoting::MonitorBehavior::InPlace); + summonArgs.ToggleVisibility(false); // Do not toggle, just make visible. // Summon the window whenever we dispatch a commandline to it. This will // make it obvious when a new tab/pane is created in a window. _HandleSummon(sender, summonArgs); @@ -935,5 +936,6 @@ void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspecta summonArgs.MoveToCurrentDesktop(false); summonArgs.DropdownDuration(0); summonArgs.ToMonitor(Remoting::MonitorBehavior::InPlace); + summonArgs.ToggleVisibility(false); // Do not toggle, just make visible. _HandleSummon(sender, summonArgs); } diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.manifest b/src/cascadia/WindowsTerminal/WindowsTerminal.manifest index aa6b42660..47612326b 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.manifest +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.manifest @@ -10,7 +10,10 @@ - + + + + diff --git a/src/cascadia/WpfTerminalTestNetCore/MainWindow.xaml.cs b/src/cascadia/WpfTerminalTestNetCore/MainWindow.xaml.cs index 9a6e70be0..e5b791b83 100644 --- a/src/cascadia/WpfTerminalTestNetCore/MainWindow.xaml.cs +++ b/src/cascadia/WpfTerminalTestNetCore/MainWindow.xaml.cs @@ -96,6 +96,8 @@ namespace WpfTerminalTestNetCore { DefaultBackground = 0x0c0c0c, DefaultForeground = 0xcccccc, + DefaultSelectionBackground = 0xcccccc, + SelectionBackgroundAlpha = 0.5f, CursorStyle = CursorStyle.BlinkingBar, // This is Campbell. ColorTable = new uint[] { 0x0C0C0C, 0x1F0FC5, 0x0EA113, 0x009CC1, 0xDA3700, 0x981788, 0xDD963A, 0xCCCCCC, 0x767676, 0x5648E7, 0x0CC616, 0xA5F1F9, 0xFF783B, 0x9E00B4, 0xD6D661, 0xF2F2F2 }, diff --git a/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest b/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest index aa6b42660..47612326b 100644 --- a/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest +++ b/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest @@ -10,7 +10,10 @@ - + + + + diff --git a/src/common.build.post.props b/src/common.build.post.props index 87416a898..e5bc2f5f3 100644 --- a/src/common.build.post.props +++ b/src/common.build.post.props @@ -64,4 +64,21 @@ + + + + OCCallFeatureFlagGenerator; + $(BuildDependsOn) + + + + + + + + + + $(SolutionDir)\bin\$(Configuration)\inc\TilFeatureStaging.h;%(ForcedIncludeFiles) + + diff --git a/src/dirs b/src/dirs index eccb84a90..f81326139 100644 --- a/src/dirs +++ b/src/dirs @@ -1,4 +1,5 @@ DIRS=\ + staging \ buffer \ interactivity \ dep \ diff --git a/src/features.xml b/src/features.xml new file mode 100644 index 000000000..7ad1db40c --- /dev/null +++ b/src/features.xml @@ -0,0 +1,52 @@ + + + + + Feature_ReceiveIncomingHandoff + OpenConsole should be able to receive incoming connections + AlwaysEnabled + + WindowsInbox + + + + + Feature_AttemptHandoff + conhost should try to hand connections over to OpenConsole + AlwaysDisabled + + WindowsInbox + + + + + Feature_ConhostDxEngine + Controls whether conhost supports the DX engine and the UseDx registry key + AlwaysEnabled + + WindowsInbox + + + + Feature_DxEngineShaderSupport + Controls whether the DX engine is built with shader support. + AlwaysEnabled + + WindowsInbox + + + + + Feature_UseNumpadEventsForClipboardInput + Controls whether the clipboard converter (and ConPTY InputStateMachine) uses Numpad events instead of UChar + AlwaysDisabled + + + WindowsInbox + + + diff --git a/src/host/ConsoleArguments.cpp b/src/host/ConsoleArguments.cpp index a95bb3800..148a6d321 100644 --- a/src/host/ConsoleArguments.cpp +++ b/src/host/ConsoleArguments.cpp @@ -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: -// - -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 diff --git a/src/host/ConsoleArguments.hpp b/src/host/ConsoleArguments.hpp index 5d0e5b31e..d82dc0bf2 100644 --- a/src/host/ConsoleArguments.hpp +++ b/src/host/ConsoleArguments.hpp @@ -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& args, const size_t index, const bool skipFirst); diff --git a/src/host/PtySignalInputThread.cpp b/src/host/PtySignalInputThread.cpp index c563adc6b..df42f45f7 100644 --- a/src/host/PtySignalInputThread.cpp +++ b/src/host/PtySignalInputThread.cpp @@ -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; @@ -26,7 +18,7 @@ using namespace Microsoft::Console::VirtualTerminal; // - Creates the PTY Signal Input Thread. // Arguments: // - hPipe - a handle to the file representing the read end of the VT pipe. -PtySignalInputThread::PtySignalInputThread(_In_ wil::unique_hfile hPipe) : +PtySignalInputThread::PtySignalInputThread(wil::unique_hfile hPipe) : _hFile{ std::move(hPipe) }, _hThread{}, _pConApi{ std::make_unique(ServiceLocator::LocateGlobals().getConsoleInformation()) }, @@ -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: // - // Return Value: @@ -70,47 +64,43 @@ DWORD WINAPI PtySignalInputThread::StaticThreadProc(_In_ LPVOID lpParameter) void PtySignalInputThread::ConnectConsole() noexcept { _consoleConnected = true; + if (_earlyResize) + { + _DoResizeWindow(*_earlyResize); + } } // Method Description: // - The ThreadProc for the PTY Signal Input Thread. // Return Value: // - S_OK if the thread runs to completion. -// - Otherwise it may cause an application termination another route and never return. +// - 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: +// - +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. diff --git a/src/host/PtySignalInputThread.hpp b/src/host/PtySignalInputThread.hpp index ea9419a32..d54cf6833 100644 --- a/src/host/PtySignalInputThread.hpp +++ b/src/host/PtySignalInputThread.hpp @@ -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 _earlyResize; std::unique_ptr _pConApi; }; } diff --git a/src/host/exe/CConsoleHandoff.cpp b/src/host/exe/CConsoleHandoff.cpp index 20ac3c6e8..eaf28088a 100644 --- a/src/host/exe/CConsoleHandoff.cpp +++ b/src/host/exe/CConsoleHandoff.cpp @@ -27,11 +27,18 @@ static HRESULT _duplicateHandle(const HANDLE in, HANDLE& out) // - server - Console driver server handle // - inputEvent - Event already established that we signal when new input data is available in case the driver is waiting on us // - msg - Portable attach message containing just enough descriptor payload to get us started in servicing it +// - inboxProcess - Handle to the inbox process so we can watch it to see if it disappears on us. +// - process - Handle to our process for waiting for us to exit HRESULT CConsoleHandoff::EstablishHandoff(HANDLE server, HANDLE inputEvent, - PCCONSOLE_PORTABLE_ATTACH_MSG msg) + PCCONSOLE_PORTABLE_ATTACH_MSG msg, + HANDLE signalPipe, + HANDLE inboxProcess, + HANDLE* process) try { + RETURN_HR_IF(E_INVALIDARG, !process); + // Fill the descriptor portion of a fresh api message with the received data. // The descriptor portion is the "received" packet from the last ask of the driver. // The other portions are unnecessary as they track the other buffer state, error codes, @@ -53,9 +60,20 @@ try // Making our own duplicate copy ensures they hang around in our lifetime. RETURN_IF_FAILED(_duplicateHandle(server, server)); RETURN_IF_FAILED(_duplicateHandle(inputEvent, inputEvent)); + RETURN_IF_FAILED(_duplicateHandle(signalPipe, signalPipe)); + RETURN_IF_FAILED(_duplicateHandle(inboxProcess, inboxProcess)); // Now perform the handoff. - RETURN_IF_FAILED(ConsoleEstablishHandoff(server, inputEvent, &apiMsg)); + RETURN_IF_FAILED(ConsoleEstablishHandoff(server, inputEvent, signalPipe, inboxProcess, &apiMsg)); + + // Give back a copy of our own process handle to be tracked. + RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), + GetCurrentProcess(), + GetCurrentProcess(), + process, + SYNCHRONIZE, + FALSE, + 0)); return S_OK; } diff --git a/src/host/exe/CConsoleHandoff.h b/src/host/exe/CConsoleHandoff.h index e2d2e14d8..36ad564f6 100644 --- a/src/host/exe/CConsoleHandoff.h +++ b/src/host/exe/CConsoleHandoff.h @@ -34,7 +34,10 @@ struct __declspec(uuid(__CLSID_CConsoleHandoff)) #pragma region IConsoleHandoff STDMETHODIMP EstablishHandoff(HANDLE server, HANDLE inputEvent, - PCCONSOLE_PORTABLE_ATTACH_MSG msg); + PCCONSOLE_PORTABLE_ATTACH_MSG msg, + HANDLE signalPipe, + HANDLE inboxProcess, + HANDLE* process); #pragma endregion }; diff --git a/src/host/exe/exemain.cpp b/src/host/exe/exemain.cpp index d7ff119ff..a85c8a313 100644 --- a/src/host/exe/exemain.cpp +++ b/src/host/exe/exemain.cpp @@ -9,7 +9,7 @@ #include "../interactivity/inc/ServiceLocator.hpp" #include "../inc/conint.h" -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED #include "CConsoleHandoff.h" #endif @@ -247,7 +247,7 @@ int CALLBACK wWinMain( // messages going forward. // 7. The out-of-box `OpenConsole.exe` can then attempt to lookup and invoke a `CTerminalHandoff` to ask a registered // Terminal to become the UI. This OpenConsole.exe will put itself in PTY mode and let the Terminal handle user interaction. -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED auto& module = OutOfProcModuleWithRegistrationFlag::Create(&_releaseNotifier); #endif @@ -264,7 +264,7 @@ int CALLBACK wWinMain( if (SUCCEEDED(hr)) { // Only try to register as a handoff target if we are NOT a part of Windows. -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED bool defAppEnabled = false; if (args.ShouldRunAsComServer() && SUCCEEDED(Microsoft::Console::Internal::DefaultApp::CheckDefaultAppPolicy(defAppEnabled)) && defAppEnabled) { diff --git a/src/host/ft_host/testmd.definition b/src/host/ft_host/testmd.definition index 09cfd7fe6..ca605c2ef 100644 --- a/src/host/ft_host/testmd.definition +++ b/src/host/ft_host/testmd.definition @@ -20,7 +20,7 @@ } ], "RemoteFiles": [ ], - "Packages": [ "Microsoft.Console.Tools.Nihilist", "microsoft-windows-test-testnetv5-0" ] + "Packages": [ "Microsoft.Console.Tools.Nihilist" ] }, "Logs": [ ], "Plugins": [ ], diff --git a/src/host/globals.h b/src/host/globals.h index c99794845..a955e991f 100644 --- a/src/host/globals.h +++ b/src/host/globals.h @@ -74,6 +74,8 @@ public: std::optional handoffConsoleClsid; std::optional handoffTerminalClsid; + wil::unique_hfile handoffInboxConsoleHandle; + wil::unique_threadpool_wait handoffInboxConsoleExitWait; #ifdef UNIT_TESTING void EnableConptyModeForTests(std::unique_ptr vtRenderEngine); diff --git a/src/host/proxy/IConsoleHandoff.idl b/src/host/proxy/IConsoleHandoff.idl index 067eb0efa..d7e8d63bc 100644 --- a/src/host/proxy/IConsoleHandoff.idl +++ b/src/host/proxy/IConsoleHandoff.idl @@ -20,11 +20,14 @@ typedef const CONSOLE_PORTABLE_ATTACH_MSG* PCCONSOLE_PORTABLE_ATTACH_MSG; [ object, - uuid(2B607BC1-43EB-40C3-95AE-2856ADDB7F23) + uuid(E686C757-9A35-4A1C-B3CE-0BCC8B5C69F4) ] interface IConsoleHandoff : IUnknown { HRESULT EstablishHandoff([in, system_handle(sh_file)] HANDLE server, [in, system_handle(sh_event)] HANDLE inputEvent, - [in, ref] PCCONSOLE_PORTABLE_ATTACH_MSG msg); + [in, ref] PCCONSOLE_PORTABLE_ATTACH_MSG msg, + [in, system_handle(sh_pipe)] HANDLE signalPipe, + [in, system_handle(sh_process)] HANDLE inboxProcess, + [out, system_handle(sh_process)] HANDLE* process); }; diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp index c5283dca4..c1ed6020d 100644 --- a/src/host/srvinit.cpp +++ b/src/host/srvinit.cpp @@ -20,6 +20,7 @@ #include "../interactivity/inc/ServiceLocator.hpp" #include "../interactivity/base/ApiDetector.hpp" +#include "../interactivity/base/RemoteConsoleControl.hpp" #include "renderData.hpp" #include "../renderer/base/renderer.hpp" @@ -27,9 +28,9 @@ #include "../inc/conint.h" #include "../propslib/DelegationConfig.hpp" -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED #include "ITerminalHandoff.h" -#endif // __INSIDE_WINDOWS +#endif // TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED #pragma hdrstop @@ -364,12 +365,14 @@ HRESULT ConsoleCreateIoThread(_In_ HANDLE Server, // from the driver... or an S_OK success. [[nodiscard]] HRESULT ConsoleEstablishHandoff([[maybe_unused]] _In_ HANDLE Server, [[maybe_unused]] HANDLE driverInputEvent, + [[maybe_unused]] HANDLE hostSignalPipe, + [[maybe_unused]] HANDLE hostProcessHandle, [[maybe_unused]] PCONSOLE_API_MSG connectMessage) try { -#ifdef __INSIDE_WINDOWS +#if !TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED); -#else // !__INSIDE_WINDOWS +#else // TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED auto& g = ServiceLocator::LocateGlobals(); g.handoffTarget = true; @@ -388,6 +391,23 @@ try return E_NOT_SET; } + // Capture handle to the inbox process into a unique handle holder. + g.handoffInboxConsoleHandle.reset(hostProcessHandle); + + // Set up a threadpool waiter to shutdown everything if the inbox process disappears. + g.handoffInboxConsoleExitWait.reset(CreateThreadpoolWait( + [](PTP_CALLBACK_INSTANCE /*callbackInstance*/, PVOID /*context*/, PTP_WAIT /*wait*/, TP_WAIT_RESULT /*waitResult*/) noexcept { + ServiceLocator::RundownAndExit(E_APPLICATION_MANAGER_NOT_RUNNING); + }, + nullptr, + nullptr)); + RETURN_LAST_ERROR_IF_NULL(g.handoffInboxConsoleExitWait.get()); + + SetThreadpoolWait(g.handoffInboxConsoleExitWait.get(), g.handoffInboxConsoleHandle.get(), nullptr); + + std::unique_ptr remoteControl = std::make_unique(hostSignalPipe); + RETURN_IF_NTSTATUS_FAILED(ServiceLocator::SetConsoleControlInstance(std::move(remoteControl))); + wil::unique_handle signalPipeTheirSide; wil::unique_handle signalPipeOurSide; @@ -397,20 +417,11 @@ try wil::unique_handle outPipeTheirSide; wil::unique_handle outPipeOurSide; - SECURITY_ATTRIBUTES sa; - sa.nLength = sizeof(sa); - // Mark inheritable for signal handle when creating. It'll have the same value on the other side. - sa.bInheritHandle = TRUE; - sa.lpSecurityDescriptor = nullptr; - RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(signalPipeOurSide.addressof(), signalPipeTheirSide.addressof(), nullptr, 0)); - RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeTheirSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(inPipeOurSide.addressof(), inPipeTheirSide.addressof(), nullptr, 0)); - RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(inPipeTheirSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(outPipeTheirSide.addressof(), outPipeOurSide.addressof(), nullptr, 0)); - RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(outPipeTheirSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); wil::unique_handle clientProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, TRUE, static_cast(connectMessage->Descriptor.Process)) }; RETURN_LAST_ERROR_IF_NULL(clientProcess.get()); @@ -434,9 +445,9 @@ try serverProcess, clientProcess.get())); - inPipeTheirSide.release(); - outPipeTheirSide.release(); - signalPipeTheirSide.release(); + inPipeTheirSide.reset(); + outPipeTheirSide.reset(); + signalPipeTheirSide.reset(); const auto commandLine = fmt::format(FMT_COMPILE(L" --headless --signal {:#x}"), (int64_t)signalPipeOurSide.release()); @@ -444,7 +455,7 @@ try RETURN_IF_FAILED(consoleArgs.ParseCommandline()); return ConsoleCreateIoThread(Server, &consoleArgs, driverInputEvent, connectMessage); -#endif // __INSIDE_WINDOWS +#endif // TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED } CATCH_RETURN() diff --git a/src/host/srvinit.h b/src/host/srvinit.h index 3b2725a39..6fac67718 100644 --- a/src/host/srvinit.h +++ b/src/host/srvinit.h @@ -30,6 +30,8 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, [[nodiscard]] HRESULT ConsoleEstablishHandoff(_In_ HANDLE Server, HANDLE driverInputEvent, + HANDLE hostSignalPipe, + HANDLE hostProcessHandle, PCONSOLE_API_MSG connectMessage); void ConsoleCheckDebug(); diff --git a/src/host/ut_host/ClipboardTests.cpp b/src/host/ut_host/ClipboardTests.cpp index 206559244..fb389757e 100644 --- a/src/host/ut_host/ClipboardTests.cpp +++ b/src/host/ut_host/ClipboardTests.cpp @@ -308,26 +308,29 @@ class ClipboardTests wstr.size()); std::deque expectedEvents; -#ifdef __INSIDE_WINDOWS - // Inside Windows, where numpad events are enabled, this generated numpad events. - // should be converted to: - // 1. left alt keydown - // 2. 1st numpad keydown - // 3. 1st numpad keyup - // 4. 2nd numpad keydown - // 5. 2nd numpad keyup - // 6. left alt keyup - expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ TRUE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ FALSE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ TRUE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ FALSE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, wstr[0], 0 }); -#else - // Outside Windows, without numpad events, we just emit the key with a nonzero UnicodeChar - expectedEvents.push_back({ TRUE, 1, 0, 0, wstr[0], 0 }); - expectedEvents.push_back({ FALSE, 1, 0, 0, wstr[0], 0 }); -#endif + if constexpr (Feature_UseNumpadEventsForClipboardInput::IsEnabled()) + { + // Inside Windows, where numpad events are enabled, this generated numpad events. + // should be converted to: + // 1. left alt keydown + // 2. 1st numpad keydown + // 3. 1st numpad keyup + // 4. 2nd numpad keydown + // 5. 2nd numpad keyup + // 6. left alt keyup + expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ TRUE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ FALSE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ TRUE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ FALSE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, wstr[0], 0 }); + } + else + { + // Outside Windows, without numpad events, we just emit the key with a nonzero UnicodeChar + expectedEvents.push_back({ TRUE, 1, 0, 0, wstr[0], 0 }); + expectedEvents.push_back({ FALSE, 1, 0, 0, wstr[0], 0 }); + } VERIFY_ARE_EQUAL(expectedEvents.size(), events.size()); diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 6497cb6bc..f180d03e0 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -12,7 +12,7 @@ #include "../Settings.hpp" #include "../VtIo.hpp" -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED #include "../../renderer/dx/DxRenderer.hpp" #endif @@ -38,7 +38,7 @@ class Microsoft::Console::VirtualTerminal::VtIoTests TEST_METHOD(RendererDtorAndThread); -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED TEST_METHOD(RendererDtorAndThreadAndDx); #endif @@ -433,7 +433,7 @@ void VtIoTests::RendererDtorAndThread() } } -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED void VtIoTests::RendererDtorAndThreadAndDx() { Log::Comment(NoThrowString().Format( diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index 08f953bb4..b17853805 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -1562,7 +1562,7 @@ void VtRendererTest::FormattedString() TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") END_TEST_METHOD_PROPERTIES(); - static const std::string format("\x1b[%dm"); + static const auto format = FMT_COMPILE("\x1b[{}m"); const auto value = 12; Viewport view = SetUpViewport(); @@ -1573,15 +1573,15 @@ void VtRendererTest::FormattedString() Log::Comment(L"1.) Write it once. It should resize itself."); qExpectedInput.push_back("\x1b[12m"); - VERIFY_SUCCEEDED(engine->_WriteFormattedString(&format, value)); + VERIFY_SUCCEEDED(engine->_WriteFormatted(format, value)); Log::Comment(L"2.) Write the same thing again, should be fine."); qExpectedInput.push_back("\x1b[12m"); - VERIFY_SUCCEEDED(engine->_WriteFormattedString(&format, value)); + VERIFY_SUCCEEDED(engine->_WriteFormatted(format, value)); Log::Comment(L"3.) Now write something huge. Should resize itself and still be fine."); - static const std::string bigFormat("\x1b[28;3;%d;%d;%dm"); + static const auto bigFormat = FMT_COMPILE("\x1b[28;3;{};{};{}m"); const auto bigValue = 500; qExpectedInput.push_back("\x1b[28;3;500;500;500m"); - VERIFY_SUCCEEDED(engine->_WriteFormattedString(&bigFormat, bigValue, bigValue, bigValue)); + VERIFY_SUCCEEDED(engine->_WriteFormatted(bigFormat, bigValue, bigValue, bigValue)); } diff --git a/src/inc/HostSignals.hpp b/src/inc/HostSignals.hpp new file mode 100644 index 000000000..05e1d0f3f --- /dev/null +++ b/src/inc/HostSignals.hpp @@ -0,0 +1,33 @@ +namespace Microsoft::Console +{ + // These values match the enumeration values of `ControlType` for the `ConsoleControl` class + // but are defined here similarly to not pollute other projects. + // They don't *have* to be the same values, but matching them seemed to make sense. + enum class HostSignals : uint8_t + { + NotifyApp = 1u, + SetForeground = 5u, + EndTask = 7u + }; + + struct HostSignalNotifyAppData + { + uint32_t sizeInBytes; + uint32_t processId; + }; + + struct HostSignalSetForegroundData + { + uint32_t sizeInBytes; + uint32_t processId; + bool isForeground; + }; + + struct HostSignalEndTaskData + { + uint32_t sizeInBytes; + uint32_t processId; + uint32_t eventType; + uint32_t ctrlFlags; + }; +}; \ No newline at end of file diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index 75bc23816..b19b42f16 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -20,38 +20,40 @@ #include #include +#include #include +#include +#include +#include +#include +#include #include -#include -#include #include +#include +#include #include -#include #include +#include #include #include +#include +#include +#include +#include #include -#include #include +#include #include #include +#include +#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include // WIL #include #include +#include #include #include #include diff --git a/src/inc/til.h b/src/inc/til.h index 9e8792b4c..509f02db9 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -19,7 +19,7 @@ #include "til/spsc.h" #include "til/coalesce.h" #include "til/replace.h" -#include "til/visualize_control_codes.h" +#include "til/string.h" #include "til/pmr.h" // Use keywords on TraceLogging providers to specify the category diff --git a/src/inc/til/latch.h b/src/inc/til/latch.h new file mode 100644 index 000000000..c24b95d5a --- /dev/null +++ b/src/inc/til/latch.h @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#ifdef __cpp_lib_latch +#include +#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::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(&counter), ¤t, sizeof(counter), INFINITE); + } + } + + void arrive_and_wait(const ptrdiff_t n = 1) noexcept + { + assert(n >= 0); + + auto old = counter.fetch_sub(n, std::memory_order_acq_rel); + if (old == n) + { + WakeByAddressAll(&counter); + return; + } + + assert(old > n); + WaitOnAddress(&counter, &old, sizeof(counter), INFINITE); + wait(); + } + + private: + std::atomic counter; + }; +#endif +} diff --git a/src/inc/til/mutex.h b/src/inc/til/mutex.h new file mode 100644 index 000000000..6882aa97d --- /dev/null +++ b/src/inc/til/mutex.h @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace til +{ + namespace details + { + template + 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 + 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>; + + // 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>; + + shared_mutex() = default; + + template + shared_mutex(Args&&... args) : + _data{ std::forward(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 diff --git a/src/inc/til/string.h b/src/inc/til/string.h new file mode 100644 index 000000000..7bcbf1410 --- /dev/null +++ b/src/inc/til/string.h @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + _TIL_INLINEPREFIX std::wstring visualize_control_codes(std::wstring str) noexcept + { + for (auto& ch : str) + { + if (ch < 0x20) + { + ch += 0x2400; + } + else if (ch == 0x20) + { + ch = 0x2423; // replace space with ␣ + } + else if (ch == 0x7f) + { + ch = 0x2421; // replace del with ␡ + } + } + return str; + } + + _TIL_INLINEPREFIX std::wstring visualize_control_codes(std::wstring_view str) + { + return visualize_control_codes(std::wstring{ str }); + } + + template + constexpr bool starts_with(const std::basic_string_view str, const std::basic_string_view prefix) noexcept + { +#ifdef __cpp_lib_starts_ends_with +#error This code can be replaced in C++20, which natively supports .starts_with(). +#endif + return str.size() >= prefix.size() && Traits::compare(str.data(), prefix.data(), prefix.size()) == 0; + }; + + constexpr bool starts_with(const std::string_view str, const std::string_view prefix) noexcept + { + return starts_with<>(str, prefix); + }; + + constexpr bool starts_with(const std::wstring_view str, const std::wstring_view prefix) noexcept + { + return starts_with<>(str, prefix); + }; + + template + constexpr bool ends_with(const std::basic_string_view str, const std::basic_string_view 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); + }; +} diff --git a/src/inc/til/throttled_func.h b/src/inc/til/throttled_func.h new file mode 100644 index 000000000..01c4e356a --- /dev/null +++ b/src/inc/til/throttled_func.h @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til +{ + namespace details + { + template + class throttled_func_storage + { + public: + template + bool emplace(MakeArgs&&... args) + { + std::unique_lock guard{ _lock }; + const bool hadValue = _pendingRunArgs.has_value(); + _pendingRunArgs.emplace(std::forward(args)...); + return hadValue; + } + + template + void modify_pending(F f) + { + std::unique_lock guard{ _lock }; + if (_pendingRunArgs) + { + std::apply(f, *_pendingRunArgs); + } + } + + std::tuple 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> _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 _isPending; + }; + } // namespace details + + template + class throttled_func + { + public: + using filetime_duration = std::chrono::duration>; + using function = std::function; + + // 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 + void operator()(MakeArgs&&... args) + { + if (!_storage.emplace(std::forward(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 + 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(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 _storage; + }; + + template + using throttled_func_trailing = throttled_func; + using throttled_func_leading = throttled_func; +} // namespace til diff --git a/src/inc/til/visualize_control_codes.h b/src/inc/til/visualize_control_codes.h deleted file mode 100644 index db5ae73d5..000000000 --- a/src/inc/til/visualize_control_codes.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -namespace til // Terminal Implementation Library. Also: "Today I Learned" -{ - _TIL_INLINEPREFIX std::wstring visualize_control_codes(std::wstring str) noexcept - { - for (auto& ch : str) - { - if (ch < 0x20) - { - ch += 0x2400; - } - else if (ch == 0x20) - { - ch = 0x2423; // replace space with ␣ - } - else if (ch == 0x7f) - { - ch = 0x2421; // replace del with ␡ - } - } - return str; - } - - _TIL_INLINEPREFIX std::wstring visualize_control_codes(std::wstring_view str) - { - return visualize_control_codes(std::wstring{ str }); - } -} diff --git a/src/interactivity/base/EventSynthesis.cpp b/src/interactivity/base/EventSynthesis.cpp index e72f80318..9d1c217d9 100644 --- a/src/interactivity/base/EventSynthesis.cpp +++ b/src/interactivity/base/EventSynthesis.cpp @@ -16,19 +16,9 @@ static constexpr WORD altScanCode = 0x38; static constexpr WORD leftShiftScanCode = 0x2A; -#ifdef __INSIDE_WINDOWS -// To reduce the risk of compatibility issues inside Windows, we're going to continue using the old -// version of GetQuickCharWidth to determine whether a character should be synthesized into numpad -// events. -static constexpr bool useNumpadEvents{ true }; -#else // !defined(__INSIDE_WINDOWS) -// In Terminal, however, we will *always* use normal key events (!) -static constexpr bool useNumpadEvents{ false }; -#endif // __INSIDE_WINDOWS - // Routine Description: // - naively determines the width of a UCS2 encoded wchar (with caveats noted above) -#pragma warning(suppress : 4505) // this function will be deleted if useNumpadEvents is false +#pragma warning(suppress : 4505) // this function will be deleted if numpad events are disabled static CodepointWidth GetQuickCharWidthLegacyForNumpadEventSynthesis(const wchar_t wch) noexcept { if ((0x1100 <= wch && wch <= 0x115f) // From Unicode 9.0, Hangul Choseong is wide @@ -62,7 +52,7 @@ std::deque> Microsoft::Console::Interactivity::CharToK if (keyState == invalidKey) { - if constexpr (useNumpadEvents) + if constexpr (Feature_UseNumpadEventsForClipboardInput::IsEnabled()) { // Determine DBCS character because these character does not know by VkKeyScan. // GetStringTypeW(CT_CTYPE3) & C3_ALPHA can determine all linguistic characters. However, this is diff --git a/src/interactivity/base/HostSignalInputThread.cpp b/src/interactivity/base/HostSignalInputThread.cpp new file mode 100644 index 000000000..2dc71ccca --- /dev/null +++ b/src/interactivity/base/HostSignalInputThread.cpp @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "HostSignalInputThread.hpp" +#include "../inc/HostSignals.hpp" + +#include "../interactivity/inc/ServiceLocator.hpp" + +using namespace Microsoft::Console; +using namespace Microsoft::Console::Interactivity; + +// Constructor Description: +// - Creates the PTY Signal Input Thread. +// Arguments: +// - hPipe - a handle to the file representing the read end of the Host Signal pipe. +HostSignalInputThread::HostSignalInputThread(wil::unique_hfile&& hPipe) : + _hFile{ std::move(hPipe) }, + _hThread{}, + _dwThreadId{} +{ + THROW_HR_IF(E_HANDLE, _hFile.get() == INVALID_HANDLE_VALUE); + THROW_HR_IF(E_HANDLE, _hFile.get() == nullptr); +} + +HostSignalInputThread::~HostSignalInputThread() +{ + // Manually terminate our thread during unittesting. Otherwise, the test + // will finish, but TAEF will not manually kill the test. +#ifdef UNIT_TESTING + TerminateThread(_hThread.get(), 0); +#endif +} + +// Function Description: +// - Static function used for initializing an instance's ThreadProc. +// Arguments: +// - lpParameter - A pointer to the HostSignalInputThread instance that should be called. +// Return Value: +// - The return value of the underlying instance's _InputThread +DWORD WINAPI HostSignalInputThread::StaticThreadProc(LPVOID lpParameter) +{ + HostSignalInputThread* const pInstance = static_cast(lpParameter); + return pInstance->_InputThread(); +} + +// Method Description: +// - Attempts to retrieve a given type T off of the internal +// pipe channel and return it. +// Return Value: +// - A structure filled with the specified data off the byte stream +// - EXCEPTIONS may be thrown if the packet size mismatches +// or if we fail to read for another reason. +template +T HostSignalInputThread::_ReceiveTypedPacket() +{ + T msg = { 0 }; + THROW_HR_IF(E_ABORT, !_GetData(gsl::as_writable_bytes(gsl::span{ &msg, 1 }))); + + // If the message is smaller than what we expected + // then it was malformed and we need to throw. + THROW_HR_IF(E_ILLEGAL_METHOD_CALL, msg.sizeInBytes < sizeof(msg)); + + // If the message size was stated to be larger, we + // want to seek forward to the next message code. + // If it's equal, we'll seek forward by 0 and + // do nothing. + _AdvanceReader(msg.sizeInBytes - sizeof(msg)); + + return msg; +} + +// Method Description: +// - The ThreadProc for the Host Signal Input Thread. +// Return Value: +// - S_OK if the thread runs to completion. +// - Otherwise it may cause an application termination and never return. +[[nodiscard]] HRESULT HostSignalInputThread::_InputThread() +{ + HostSignals signalId; + + while (_GetData(gsl::as_writable_bytes(gsl::span{ &signalId, 1 }))) + { + switch (signalId) + { + case HostSignals::NotifyApp: + { + auto msg = _ReceiveTypedPacket(); + + LOG_IF_NTSTATUS_FAILED(ServiceLocator::LocateConsoleControl()->NotifyConsoleApplication(msg.processId)); + + break; + } + case HostSignals::SetForeground: + { + auto msg = _ReceiveTypedPacket(); + + LOG_IF_NTSTATUS_FAILED(ServiceLocator::LocateConsoleControl()->SetForeground(ULongToHandle(msg.processId), msg.isForeground)); + + break; + } + case HostSignals::EndTask: + { + auto msg = _ReceiveTypedPacket(); + + LOG_IF_NTSTATUS_FAILED(ServiceLocator::LocateConsoleControl()->EndTask(ULongToHandle(msg.processId), msg.eventType, msg.ctrlFlags)); + + break; + } + default: + { + THROW_HR(E_UNEXPECTED); + break; + } + } + } + return S_OK; +} + +// Method Description: +// - Skips the file stream forward by the specified number of bytes. +// Arguments: +// - byteCount - Count of bytes to skip forward +// Return Value: +// - True if we could skip forward successfully. False otherwise. +bool HostSignalInputThread::_AdvanceReader(DWORD byteCount) +{ + std::array buffer; + + while (byteCount > 0) + { + const auto advance = std::min(byteCount, gsl::narrow_cast(buffer.max_size())); + + if (!_GetData(buffer)) + { + return false; + } + + byteCount -= advance; + } + + return true; +} + +// Method Description: +// - Retrieves bytes from the file stream and exits or throws errors should the pipe state +// be compromised. +// Arguments: +// - buffer - Buffer to fill with data. +// Return Value: +// - True if data was retrieved successfully. False otherwise. +bool HostSignalInputThread::_GetData(gsl::span buffer) +{ + DWORD bytesRead = 0; + // If we failed to read because the terminal broke our pipe (usually due + // to dying itself), close gracefully with ERROR_BROKEN_PIPE. + // Otherwise throw an exception. ERROR_BROKEN_PIPE is the only case that + // we want to gracefully close in. + if (FALSE == ReadFile(_hFile.get(), buffer.data(), gsl::narrow_cast(buffer.size()), &bytesRead, nullptr)) + { + DWORD lastError = GetLastError(); + if (lastError == ERROR_BROKEN_PIPE) + { + _Shutdown(); + return false; + } + + THROW_WIN32(lastError); + } + + if (bytesRead != buffer.size()) + { + _Shutdown(); + return false; + } + + return true; +} + +// Method Description: +// - Starts the PTY Signal input thread. +[[nodiscard]] HRESULT HostSignalInputThread::Start() noexcept +{ + // 0 is the right value, https://devblogs.microsoft.com/oldnewthing/20040223-00/?p=40503 + _dwThreadId = 0; + + _hThread.reset(CreateThread(nullptr, + 0, + HostSignalInputThread::StaticThreadProc, + this, + 0, + &_dwThreadId)); + + RETURN_LAST_ERROR_IF_NULL(_hThread.get()); + LOG_IF_FAILED(SetThreadDescription(_hThread.get(), L"Host Signal Handler Thread")); + + return S_OK; +} + +// Method Description: +// - Perform a shutdown of the console. This happens when the signal pipe is +// broken, which means either the parent terminal process has died, or they +// called ClosePseudoConsole. +// CloseConsoleProcessState is not enough by itself - it will disconnect +// clients as if the X was pressed, but then we need to actually still die, +// so then call RundownAndExit. +// Arguments: +// - +// Return Value: +// - +void HostSignalInputThread::_Shutdown() +{ + // Make sure we terminate. + ServiceLocator::RundownAndExit(ERROR_BROKEN_PIPE); +} diff --git a/src/interactivity/base/HostSignalInputThread.hpp b/src/interactivity/base/HostSignalInputThread.hpp new file mode 100644 index 000000000..429e49295 --- /dev/null +++ b/src/interactivity/base/HostSignalInputThread.hpp @@ -0,0 +1,48 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- HostSignalInputThread.hpp + +Abstract: +- Defines methods that wrap the thread that will wait for signals + from a delegated console host to this "owner" console. + +Author(s): +- Michael Niksa (miniksa) 10 Jun 2021 + +Notes: +- Sourced from `PtySignalInputThread` +--*/ +#pragma once + +namespace Microsoft::Console +{ + class HostSignalInputThread final + { + public: + HostSignalInputThread(wil::unique_hfile&& hPipe); + ~HostSignalInputThread(); + + [[nodiscard]] HRESULT Start() noexcept; + static DWORD WINAPI StaticThreadProc(LPVOID lpParameter); + + // Prevent copying and assignment. + HostSignalInputThread(const HostSignalInputThread&) = delete; + HostSignalInputThread& operator=(const HostSignalInputThread&) = delete; + + private: + template + T _ReceiveTypedPacket(); + [[nodiscard]] HRESULT _InputThread(); + + bool _GetData(gsl::span buffer); + bool _AdvanceReader(DWORD byteCount); + void _Shutdown(); + + DWORD _dwThreadId; + wil::unique_hfile _hFile; + wil::unique_handle _hThread; + }; +} diff --git a/src/interactivity/base/RemoteConsoleControl.cpp b/src/interactivity/base/RemoteConsoleControl.cpp new file mode 100644 index 000000000..21b8155a7 --- /dev/null +++ b/src/interactivity/base/RemoteConsoleControl.cpp @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "RemoteConsoleControl.hpp" + +#include "../../inc/HostSignals.hpp" + +using namespace Microsoft::Console::Interactivity; + +RemoteConsoleControl::RemoteConsoleControl(HANDLE signalPipe) : + _pipe{ signalPipe } +{ +} + +#pragma region IConsoleControl Members + +template +[[nodiscard]] NTSTATUS _SendTypedPacket(HANDLE pipe, ::Microsoft::Console::HostSignals signalCode, T& payload) +{ + // To ensure it's a happy wire format, pack it tight at 1. +#pragma pack(push, 1) + struct HostSignalPacket + { + ::Microsoft::Console::HostSignals code; + T data; + }; +#pragma pack(pop) + + HostSignalPacket packet; + packet.code = signalCode; + packet.data = payload; + + DWORD bytesWritten = 0; + if (!WriteFile(pipe, &packet, sizeof(packet), &bytesWritten, nullptr)) + { + NT_RETURN_NTSTATUS(static_cast(NTSTATUS_FROM_WIN32(::GetLastError()))); + } + + if (bytesWritten != sizeof(packet)) + { + NT_RETURN_NTSTATUS(static_cast(NTSTATUS_FROM_WIN32(E_UNEXPECTED))); + } + + return STATUS_SUCCESS; +} + +[[nodiscard]] NTSTATUS RemoteConsoleControl::NotifyConsoleApplication(_In_ DWORD dwProcessId) +{ + HostSignalNotifyAppData data{}; + data.sizeInBytes = sizeof(data); + data.processId = dwProcessId; + + return _SendTypedPacket(_pipe.get(), HostSignals::NotifyApp, data); +} + +[[nodiscard]] NTSTATUS RemoteConsoleControl::SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground) +{ + HostSignalSetForegroundData data{}; + data.sizeInBytes = sizeof(data); + data.processId = HandleToULong(hProcess); + data.isForeground = fForeground; + + return _SendTypedPacket(_pipe.get(), HostSignals::SetForeground, data); +} + +[[nodiscard]] NTSTATUS RemoteConsoleControl::EndTask(_In_ HANDLE hProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags) +{ + HostSignalEndTaskData data{}; + data.sizeInBytes = sizeof(data); + data.processId = HandleToULong(hProcessId); + data.eventType = dwEventType; + data.ctrlFlags = ulCtrlFlags; + + return _SendTypedPacket(_pipe.get(), HostSignals::EndTask, data); +} + +#pragma endregion diff --git a/src/interactivity/base/RemoteConsoleControl.hpp b/src/interactivity/base/RemoteConsoleControl.hpp new file mode 100644 index 000000000..52de8db1e --- /dev/null +++ b/src/interactivity/base/RemoteConsoleControl.hpp @@ -0,0 +1,33 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- RemoteConsoleControl.hpp + +Abstract: +- This module is used for remoting console control calls to a different host owner process. + +Author(s): +- Michael Niksa (MiNiksa) 10-Jun-2021 +--*/ +#pragma once + +#include "../inc/IConsoleControl.hpp" + +namespace Microsoft::Console::Interactivity +{ + class RemoteConsoleControl final : public IConsoleControl + { + public: + RemoteConsoleControl(HANDLE signalPipe); + + // IConsoleControl Members + [[nodiscard]] NTSTATUS NotifyConsoleApplication(_In_ DWORD dwProcessId); + [[nodiscard]] NTSTATUS SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground); + [[nodiscard]] NTSTATUS EndTask(_In_ HANDLE hProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags); + + private: + wil::unique_handle _pipe; + }; +} diff --git a/src/interactivity/base/ServiceLocator.cpp b/src/interactivity/base/ServiceLocator.cpp index 14d088368..40fad8e9c 100644 --- a/src/interactivity/base/ServiceLocator.cpp +++ b/src/interactivity/base/ServiceLocator.cpp @@ -104,6 +104,24 @@ void ServiceLocator::RundownAndExit(const HRESULT hr) #pragma region Set Methods +[[nodiscard]] NTSTATUS ServiceLocator::SetConsoleControlInstance(_In_ std::unique_ptr&& control) +{ + if (s_consoleControl) + { + NT_RETURN_NTSTATUS(STATUS_INVALID_HANDLE); + } + else if (!control) + { + NT_RETURN_NTSTATUS(STATUS_INVALID_PARAMETER); + } + else + { + s_consoleControl = std::move(control); + } + + return STATUS_SUCCESS; +} + [[nodiscard]] NTSTATUS ServiceLocator::SetConsoleWindowInstance(_In_ IConsoleWindow* window) { NTSTATUS status = STATUS_SUCCESS; diff --git a/src/interactivity/base/lib/InteractivityBase.vcxproj b/src/interactivity/base/lib/InteractivityBase.vcxproj index bf64e6cf4..0986a133f 100644 --- a/src/interactivity/base/lib/InteractivityBase.vcxproj +++ b/src/interactivity/base/lib/InteractivityBase.vcxproj @@ -6,7 +6,7 @@ Base InteractivityBase ConInteractivityBaseLib - StaticLibrary + StaticLibrary @@ -21,11 +21,13 @@ - + + Create + @@ -40,12 +42,14 @@ - + + + - + \ No newline at end of file diff --git a/src/interactivity/base/lib/InteractivityBase.vcxproj.filters b/src/interactivity/base/lib/InteractivityBase.vcxproj.filters index 58825ba6f..b7f9e8e18 100644 --- a/src/interactivity/base/lib/InteractivityBase.vcxproj.filters +++ b/src/interactivity/base/lib/InteractivityBase.vcxproj.filters @@ -27,9 +27,15 @@ Source Files - - Source Files - + + Source Files + + + Source Files + + + Source Files + @@ -41,9 +47,6 @@ Header Files - - Source Files - Header Files @@ -77,9 +80,18 @@ Header Files - - Header Files - + + Header Files + + + Header Files + + + Header Files + + + Header Files + diff --git a/src/interactivity/base/sources.inc b/src/interactivity/base/sources.inc index 4881f7911..9521ea7c0 100644 --- a/src/interactivity/base/sources.inc +++ b/src/interactivity/base/sources.inc @@ -43,7 +43,9 @@ SOURCES = \ ..\InteractivityFactory.cpp \ ..\ServiceLocator.cpp \ ..\VtApiRedirection.cpp \ - ..\EventSynthesis.cpp \ + ..\EventSynthesis.cpp \ + ..\RemoteConsoleControl.cpp \ + ..\HostSignalInputThread.cpp \ INCLUDES = \ $(INCLUDES); \ diff --git a/src/interactivity/inc/ServiceLocator.hpp b/src/interactivity/inc/ServiceLocator.hpp index 888855b4a..a7b0dc89a 100644 --- a/src/interactivity/inc/ServiceLocator.hpp +++ b/src/interactivity/inc/ServiceLocator.hpp @@ -39,6 +39,7 @@ namespace Microsoft::Console::Interactivity static IAccessibilityNotifier* LocateAccessibilityNotifier(); + [[nodiscard]] static NTSTATUS SetConsoleControlInstance(_In_ std::unique_ptr&& control); static IConsoleControl* LocateConsoleControl(); template static T* LocateConsoleControl() diff --git a/src/interactivity/win32/ConsoleControl.hpp b/src/interactivity/win32/ConsoleControl.hpp index aa8b10916..9a8ab927f 100644 --- a/src/interactivity/win32/ConsoleControl.hpp +++ b/src/interactivity/win32/ConsoleControl.hpp @@ -3,7 +3,7 @@ Copyright (c) Microsoft Corporation Licensed under the MIT license. Module Name: -- userdpiapi.hpp +- ConsoleControl.hpp Abstract: - This module is used for abstracting calls to private user32 DLL APIs to break the build system dependency. diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index 8bd5d0ae4..75235fc94 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -29,7 +29,7 @@ #include "../../renderer/base/renderer.hpp" #include "../../renderer/gdi/gdirenderer.hpp" -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED #include "../../renderer/dx/DxRenderer.hpp" #else // Forward-declare this so we don't blow up later. @@ -217,7 +217,7 @@ void Window::_UpdateSystemMetrics() const [[maybe_unused]] DxEngine* pDxEngine = nullptr; try { -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED if (useDx) { pDxEngine = new DxEngine(); @@ -324,7 +324,7 @@ void Window::_UpdateSystemMetrics() const { _hWnd = hWnd; -#ifndef __INSIDE_WINDOWS +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED if (useDx) { status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pDxEngine->SetHwnd(hWnd)))); diff --git a/src/project.inc b/src/project.inc index 41fd0a480..8c3f92206 100644 --- a/src/project.inc +++ b/src/project.inc @@ -37,6 +37,8 @@ USER_C_FLAGS = $(USER_C_FLAGS) /utf-8 CONSOLE_SRC_PATH = $(PROJECT_ROOT)\core\console\open\src CONSOLE_OBJ_PATH = $(WINCORE_OBJ_PATH)\console\open\src +USER_C_FLAGS = $(USER_C_FLAGS) /FI$(CONSOLE_OBJ_PATH)\staging\$(O)\TilFeatureStaging.h + INCLUDES= \ $(INCLUDES); \ $(CONSOLE_SRC_PATH)\inc; \ diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index f051026d5..0e25d96a2 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -133,10 +133,21 @@ CATCH_RETURN() _In_ IDWriteTextRenderer* renderer, FLOAT originX, FLOAT originY) noexcept +try { const auto drawingContext = static_cast(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 diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp new file mode 100644 index 000000000..03c224868 --- /dev/null +++ b/src/renderer/dx/DxFontInfo.cpp @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "DxFontInfo.h" + +#include "unicode.hpp" + +#include + +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(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 DxFontInfo::ResolveFontFaceWithFallback(gsl::not_null dwriteFactory, + std::wstring& localeName) +{ + // First attempt to find exactly what the user asked for. + _didFallback = false; + Microsoft::WRL::ComPtr 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 DxFontInfo::_FindFontFace(gsl::not_null dwriteFactory, std::wstring& localeName, const bool withNearbyLookup) +{ + Microsoft::WRL::ComPtr fontFace; + + Microsoft::WRL::ComPtr 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 fontFamily; + THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); + + Microsoft::WRL::ComPtr font; + THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(GetWeight(), GetStretch(), GetStyle(), &font)); + + Microsoft::WRL::ComPtr 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 const fontFamily, + std::wstring& localeName) +{ + // See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection + Microsoft::WRL::ComPtr 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& DxFontInfo::_NearbyCollection(gsl::not_null 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 factory3; + THROW_IF_FAILED(dwriteFactory->QueryInterface(&factory3)); + + ::Microsoft::WRL::ComPtr fontSetBuilder; + THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder)); + + // Builder2 has a convenience to just feed in paths to font files. + ::Microsoft::WRL::ComPtr fontSetBuilder2; + THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2)); + + for (auto& p : knownPaths) + { + fontSetBuilder2->AddFontFile(p.c_str()); + } + + ::Microsoft::WRL::ComPtr 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: +// - +// Return Value: +// - Iterable collection of filesystem paths, one per font file that was found +[[nodiscard]] std::vector DxFontInfo::s_GetNearbyFonts() +{ + std::vector 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(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; +} diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h new file mode 100644 index 000000000..924202632 --- /dev/null +++ b/src/renderer/dx/DxFontInfo.h @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include +#include + +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 ResolveFontFaceWithFallback(gsl::not_null dwriteFactory, + std::wstring& localeName); + + private: + [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(gsl::not_null dwriteFactory, + std::wstring& localeName, + const bool withNearbyLookup); + + [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily, + std::wstring& localeName); + + [[nodiscard]] const Microsoft::WRL::ComPtr& _NearbyCollection(gsl::not_null dwriteFactory) const; + + [[nodiscard]] static std::vector s_GetNearbyFonts(); + + mutable ::Microsoft::WRL::ComPtr _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 + { + size_t operator()(const Microsoft::Console::Render::DxFontInfo& fontInfo) const noexcept + { + const size_t h1 = std::hash{}(fontInfo.GetFamilyName()); + const size_t h2 = std::hash{}(fontInfo.GetWeight()); + const size_t h3 = std::hash{}(fontInfo.GetStyle()); + const size_t h4 = std::hash{}(fontInfo.GetStretch()); + const size_t h5 = std::hash{}(fontInfo.GetFallback()); + + static const auto combine = [](std::initializer_list 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 }); + } + }; +} diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index d9360525e..20340ba6d 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -17,14 +17,22 @@ using namespace Microsoft::Console::Render; DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept : _dwriteFactory(dwriteFactory), + _fontSize{}, _glyphCell{}, - _lineMetrics({}), - _boxDrawingEffect{} + _lineMetrics{}, + _lineSpacing{} { } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::Analyzer() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::Analyzer() { + if (!_dwriteTextAnalyzer) + { + Microsoft::WRL::ComPtr analyzer; + THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer)); + THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer)); + } + return _dwriteTextAnalyzer; } @@ -40,49 +48,24 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr 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: -// - -// Return Value: -// - DirectWrite font collection. May be null if one cannot be created. -[[nodiscard]] const Microsoft::WRL::ComPtr& 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 factory3; - THROW_IF_FAILED(_dwriteFactory.As(&factory3)); + std::array localeName; - ::Microsoft::WRL::ComPtr fontSetBuilder; - THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder)); - - // Builder2 has a convenience to just feed in paths to font files. - ::Microsoft::WRL::ComPtr fontSetBuilder2; - THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2)); - - for (auto& p : knownPaths) + const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size())); + if (returnCode) { - fontSetBuilder2->AddFontFile(p.c_str()); + _userLocaleName = { localeName.data() }; + } + else + { + _userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() }; } - - ::Microsoft::WRL::ComPtr 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 dwr return _lineMetrics; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() noexcept +[[nodiscard]] DWRITE_FONT_WEIGHT DxFontRenderData::DefaultFontWeight() noexcept { - return _dwriteTextFormat; + return _defaultFontInfo.GetWeight(); } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() noexcept +[[nodiscard]] DWRITE_FONT_STYLE DxFontRenderData::DefaultFontStyle() noexcept { - return _dwriteFontFace; + return _defaultFontInfo.GetStyle(); } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultBoxDrawingEffect() noexcept +[[nodiscard]] DWRITE_FONT_STRETCH DxFontRenderData::DefaultFontStretch() noexcept { + return _defaultFontInfo.GetStretch(); +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() +{ + return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() +{ + return FontFaceWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); +} + +[[nodiscard]] Microsoft::WRL::ComPtr 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 DxFontRenderData::ItalicTextFormat() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr 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 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 DxFontRenderData::ItalicFontFace() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr 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 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 dwr try { _userLocaleName.clear(); + _textFormatMap.clear(); + _fontFaceMap.clear(); + _boxDrawingEffect.Reset(); - std::wstring fontName(desired.GetFaceName()); - DWRITE_FONT_WEIGHT weight = static_cast(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(desired.GetEngineSize().Y) * static_cast(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(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); - - const float widthAdvance = static_cast(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 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 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 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(widthExact); - coordSize.Y = gsl::narrow_cast(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 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 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 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(desired.GetEngineSize().Y) * static_cast(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(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); + + const float widthAdvance = static_cast(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(widthExact); + coordSize.Y = gsl::narrow_cast(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 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 DxFontRenderData::_BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName) { - Microsoft::WRL::ComPtr fontFace; - - Microsoft::WRL::ComPtr 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 fontFamily; - THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); - - Microsoft::WRL::ComPtr font; - THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font)); - - Microsoft::WRL::ComPtr 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 const fontFamily, - std::wstring& localeName) const -{ - // See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection - Microsoft::WRL::ComPtr 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 localeName; - - const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(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: -// - -// Return Value: -// - Iterable collection of filesystem paths, one per font file that was found -[[nodiscard]] std::vector DxFontRenderData::s_GetNearbyFonts() -{ - std::vector 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(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 format; + THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontInfo.GetFamilyName().data(), + nullptr, + fontInfo.GetWeight(), + fontInfo.GetStyle(), + fontInfo.GetStretch(), + _fontSize, + localeName.data(), + &format)); + return format; } diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index b0ed21391..990bcbd96 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -4,6 +4,7 @@ #pragma once #include "../../renderer/inc/FontInfoDesired.hpp" +#include "DxFontInfo.h" #include "BoxDrawingEffect.h" #include @@ -31,73 +32,69 @@ namespace Microsoft::Console::Render DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept; // DirectWrite text analyzer from the factory - [[nodiscard]] Microsoft::WRL::ComPtr Analyzer() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr Analyzer(); [[nodiscard]] Microsoft::WRL::ComPtr SystemFontFallback(); - [[nodiscard]] const Microsoft::WRL::ComPtr& 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 DefaultTextFormat() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat(); // The DirectWrite font face to use while calculating layout (by default) - [[nodiscard]] Microsoft::WRL::ComPtr DefaultFontFace() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr DefaultFontFace(); // Box drawing scaling effects that are cached for the base font across layouts - [[nodiscard]] Microsoft::WRL::ComPtr DefaultBoxDrawingEffect() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr DefaultBoxDrawingEffect(); - // The italic variant of the format object representing the size and other text properties for italic text - [[nodiscard]] Microsoft::WRL::ComPtr ItalicTextFormat() noexcept; + // The attributed variants of the format object representing the size and other text properties + [[nodiscard]] Microsoft::WRL::ComPtr 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 ItalicFontFace() noexcept; + // The attributed variants of the font face to use while calculating layout + [[nodiscard]] Microsoft::WRL::ComPtr 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 _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 _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 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 s_GetNearbyFonts(); + void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi); + Microsoft::WRL::ComPtr _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName); ::Microsoft::WRL::ComPtr _dwriteFactory; ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; - ::Microsoft::WRL::ComPtr _dwriteTextFormat; - ::Microsoft::WRL::ComPtr _dwriteTextFormatItalic; - ::Microsoft::WRL::ComPtr _dwriteFontFace; - ::Microsoft::WRL::ComPtr _dwriteFontFaceItalic; + + std::unordered_map> _textFormatMap; + std::unordered_map> _fontFaceMap; ::Microsoft::WRL::ComPtr _boxDrawingEffect; ::Microsoft::WRL::ComPtr _systemFontFallback; mutable ::Microsoft::WRL::ComPtr _nearbyCollection; std::wstring _userLocaleName; + DxFontInfo _defaultFontInfo; + float _fontSize; til::size _glyphCell; - + DWRITE_LINE_SPACING _lineSpacing; LineMetrics _lineMetrics; }; } diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 2b148c68c..c94a7147e 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -194,7 +194,7 @@ _CompileShader( std::string target, std::string entry = "main") { -#ifdef __INSIDE_WINDOWS +#if !TIL_FEATURE_DXENGINESHADERSUPPORT_ENABLED THROW_HR(E_UNEXPECTED); return 0; #else diff --git a/src/renderer/dx/ScreenPixelShader.h b/src/renderer/dx/ScreenPixelShader.h index 0cbf185e2..fa5322057 100644 --- a/src/renderer/dx/ScreenPixelShader.h +++ b/src/renderer/dx/ScreenPixelShader.h @@ -1,6 +1,6 @@ #pragma once -#ifdef __INSIDE_WINDOWS +#if !TIL_FEATURE_DXENGINESHADERSUPPORT_ENABLED constexpr std::string_view retroPixelShaderString{ "" }; #else constexpr std::string_view retroPixelShaderString{ R"( diff --git a/src/renderer/dx/ScreenVertexShader.h b/src/renderer/dx/ScreenVertexShader.h index f834c18d2..c0a4c8eaf 100644 --- a/src/renderer/dx/ScreenVertexShader.h +++ b/src/renderer/dx/ScreenVertexShader.h @@ -1,6 +1,6 @@ #pragma once -#ifdef __INSIDE_WINDOWS +#if !TIL_FEATURE_DXENGINESHADERSUPPORT_ENABLED const char screenVertexShaderString[] = ""; #else const char screenVertexShaderString[] = R"( diff --git a/src/renderer/dx/lib/dx.vcxproj b/src/renderer/dx/lib/dx.vcxproj index 61f2ca249..618d1b9ce 100644 --- a/src/renderer/dx/lib/dx.vcxproj +++ b/src/renderer/dx/lib/dx.vcxproj @@ -21,6 +21,7 @@ Create + @@ -30,6 +31,7 @@ + diff --git a/src/renderer/dx/precomp.h b/src/renderer/dx/precomp.h index d3d51af5f..9a09c25bb 100644 --- a/src/renderer/dx/precomp.h +++ b/src/renderer/dx/precomp.h @@ -15,11 +15,8 @@ #include -#include #include -#include #include -#include #include diff --git a/src/renderer/dx/sources.inc b/src/renderer/dx/sources.inc index 328779d27..f94f2d5ba 100644 --- a/src/renderer/dx/sources.inc +++ b/src/renderer/dx/sources.inc @@ -33,6 +33,7 @@ INCLUDES = \ SOURCES = \ $(SOURCES) \ ..\DxRenderer.cpp \ + ..\DxFontInfo.cpp \ ..\DxFontRenderData.cpp \ ..\CustomTextRenderer.cpp \ ..\CustomTextLayout.cpp \ diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 9295d3d6d..5c948efaa 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -436,9 +436,14 @@ using namespace Microsoft::Console::Render; if (_cPolyText > 0) { - if (!PolyTextOutW(_hdcMemoryContext, _pPolyText, (UINT)_cPolyText)) + for (size_t i = 0; i != _cPolyText; ++i) { - hr = E_FAIL; + const auto& t = _pPolyText[i]; + if (!ExtTextOutW(_hdcMemoryContext, t.x, t.y, t.uiFlags, &t.rcl, t.lpstr, t.n, t.pdx)) + { + hr = E_FAIL; + break; + } } _polyStrings.clear(); diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp index 157d95102..efd16e2cb 100644 --- a/src/renderer/vt/VtSequences.cpp +++ b/src/renderer/vt/VtSequences.cpp @@ -81,8 +81,7 @@ using namespace Microsoft::Console::Render; // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::_EraseCharacter(const short chars) noexcept { - static const std::string format = "\x1b[%dX"; - return _WriteFormattedString(&format, chars); + return _WriteFormatted(FMT_COMPILE("\x1b[{}X"), chars); } // Method Description: @@ -93,8 +92,7 @@ using namespace Microsoft::Console::Render; // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::_CursorForward(const short chars) noexcept { - static const std::string format = "\x1b[%dC"; - return _WriteFormattedString(&format, chars); + return _WriteFormatted(FMT_COMPILE("\x1b[{}C"), chars); } // Method Description: @@ -132,9 +130,8 @@ using namespace Microsoft::Console::Render; { return _Write(fInsertLine ? "\x1b[L" : "\x1b[M"); } - const std::string format = fInsertLine ? "\x1b[%dL" : "\x1b[%dM"; - return _WriteFormattedString(&format, sLines); + return _WriteFormatted(FMT_COMPILE("\x1b[{}{}"), sLines, fInsertLine ? 'L' : 'M'); } // Method Description: @@ -171,14 +168,12 @@ using namespace Microsoft::Console::Render; // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::_CursorPosition(const COORD coord) noexcept { - static const std::string cursorFormat = "\x1b[%d;%dH"; - // VT coords start at 1,1 COORD coordVt = coord; coordVt.X++; coordVt.Y++; - return _WriteFormattedString(&cursorFormat, coordVt.Y, coordVt.X); + return _WriteFormatted(FMT_COMPILE("\x1b[{};{}H"), coordVt.Y, coordVt.X); } // Method Description: @@ -213,8 +208,6 @@ using namespace Microsoft::Console::Render; [[nodiscard]] HRESULT VtEngine::_SetGraphicsRendition16Color(const WORD wAttr, const bool fIsForeground) noexcept { - static const std::string fmt = "\x1b[%dm"; - // Always check using the foreground flags, because the bg flags constants // are a higher byte // Foreground sequences are in [30,37] U [90,97] @@ -234,7 +227,7 @@ using namespace Microsoft::Console::Render; (WI_IsFlagSet(wAttr, FOREGROUND_GREEN) ? 2 : 0) + (WI_IsFlagSet(wAttr, FOREGROUND_BLUE) ? 4 : 0); - return _WriteFormattedString(&fmt, vtIndex); + return _WriteFormatted(FMT_COMPILE("\x1b[{}m"), vtIndex); } // Method Description: @@ -248,11 +241,7 @@ using namespace Microsoft::Console::Render; [[nodiscard]] HRESULT VtEngine::_SetGraphicsRendition256Color(const WORD index, const bool fIsForeground) noexcept { - const std::string fmt = fIsForeground ? - "\x1b[38;5;%dm" : - "\x1b[48;5;%dm"; - - return _WriteFormattedString(&fmt, ::Xterm256ToWindowsIndex(index)); + return _WriteFormatted(FMT_COMPILE("\x1b[{}8;5;{}m"), fIsForeground ? '3' : '4', ::Xterm256ToWindowsIndex(index)); } // Method Description: @@ -266,15 +255,10 @@ using namespace Microsoft::Console::Render; [[nodiscard]] HRESULT VtEngine::_SetGraphicsRenditionRGBColor(const COLORREF color, const bool fIsForeground) noexcept { - const std::string fmt = fIsForeground ? - "\x1b[38;2;%d;%d;%dm" : - "\x1b[48;2;%d;%d;%dm"; - - DWORD const r = GetRValue(color); - DWORD const g = GetGValue(color); - DWORD const b = GetBValue(color); - - return _WriteFormattedString(&fmt, r, g, b); + const uint8_t r = GetRValue(color); + const uint8_t g = GetGValue(color); + const uint8_t b = GetBValue(color); + return _WriteFormatted(FMT_COMPILE("\x1b[{}8;2;{};{};{}m"), fIsForeground ? '3' : '4', r, g, b); } // Method Description: @@ -286,9 +270,7 @@ using namespace Microsoft::Console::Render; // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::_SetGraphicsRenditionDefaultColor(const bool fIsForeground) noexcept { - const std::string_view fmt = fIsForeground ? ("\x1b[39m") : ("\x1b[49m"); - - return _Write(fmt); + return _Write(fIsForeground ? ("\x1b[39m") : ("\x1b[49m")); } // Method Description: @@ -300,13 +282,12 @@ using namespace Microsoft::Console::Render; // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::_ResizeWindow(const short sWidth, const short sHeight) noexcept { - static const std::string resizeFormat = "\x1b[8;%d;%dt"; if (sWidth < 0 || sHeight < 0) { return E_INVALIDARG; } - return _WriteFormattedString(&resizeFormat, sHeight, sWidth); + return _WriteFormatted(FMT_COMPILE("\x1b[8;{};{}t"), sHeight, sWidth); } // Method Description: @@ -329,8 +310,7 @@ using namespace Microsoft::Console::Render; // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::_ChangeTitle(_In_ const std::string& title) noexcept { - const std::string titleFormat = "\x1b]0;" + title + "\x7"; - return _Write(titleFormat); + return _WriteFormatted(FMT_COMPILE("\x1b]0;{}\x7"), title); } // Method Description: @@ -472,19 +452,17 @@ using namespace Microsoft::Console::Render; // send the auto-assigned ID, prefixed with the PID of this session // (we do this so different conpty sessions do not overwrite each other's hyperlinks) const auto sessionID = GetCurrentProcessId(); - const std::string uri_str{ til::u16u8(uri) }; - auto s = fmt::format(FMT_COMPILE("\x1b]8;id={}-{};{}\x1b\\"), sessionID, numberId, uri_str); - return _Write(s); + const auto uriStr = til::u16u8(uri); + return _WriteFormatted(FMT_COMPILE("\x1b]8;id={}-{};{}\x1b\\"), sessionID, numberId, uriStr); } else { // This is the case of user-defined IDs: // send the user-defined ID, prefixed with a "u" // (we do this so no application can accidentally override a user defined ID) - const std::string uri_str{ til::u16u8(uri) }; - const std::string customId_str{ til::u16u8(customId) }; - auto s = fmt::format(FMT_COMPILE("\x1b]8;id=u-{};{}\x1b\\"), customId_str, uri_str); - return _Write(s); + const auto uriStr = til::u16u8(uri); + const auto customIdStr = til::u16u8(customId); + return _WriteFormatted(FMT_COMPILE("\x1b]8;id=u-{};{}\x1b\\"), customIdStr, uriStr); } } diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index a324e988b..5fc185338 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -115,7 +115,7 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, if (!_pipeBroken) { - bool fSuccess = !!WriteFile(_hFile.get(), _buffer.data(), static_cast(_buffer.size()), nullptr, nullptr); + bool fSuccess = !!WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast(_buffer.size()), nullptr, nullptr); _buffer.clear(); if (!fSuccess) { @@ -178,71 +178,6 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, return _Write(needed); } -// Method Description: -// - Helper for calling _Write with a string for formatting a sequence. Used -// extensively by VtSequences.cpp -// Arguments: -// - pFormat: pointer to format string to write to the pipe -// - ...: a va_list of args to format the string with. -// Return Value: -// - S_OK, E_INVALIDARG for a invalid format string, or suitable HRESULT error -// from writing pipe. -[[nodiscard]] HRESULT VtEngine::_WriteFormattedString(const std::string* const pFormat, ...) noexcept -try -{ - va_list args; - va_start(args, pFormat); - - // NOTE: pFormat is a pointer because varargs refuses to operate with a ref in that position - // NOTE: We're not using string_view because it doesn't guarantee null (which will be needed - // later in the formatting method). - - HRESULT hr = E_FAIL; - - // We're going to hold onto our format string space across calls because - // the VT renderer will be formatting a LOT of strings and alloc/freeing them - // over and over is going to be way worse for perf than just holding some extra - // memory for formatting purposes. - // See _formatBuffer for its location. - - // First, plow ahead using our pre-reserved string space. - LPSTR destEnd = nullptr; - size_t destRemaining = 0; - if (SUCCEEDED(StringCchVPrintfExA(_formatBuffer.data(), - _formatBuffer.size(), - &destEnd, - &destRemaining, - STRSAFE_NO_TRUNCATION, - pFormat->c_str(), - args))) - { - return _Write({ _formatBuffer.data(), _formatBuffer.size() - destRemaining }); - } - - // If we didn't succeed at filling/using the existing space, then - // we're going to take the long way by counting the space required and resizing up to that - // space and formatting. - - const auto needed = _scprintf(pFormat->c_str(), args); - // -1 is the _scprintf error case https://msdn.microsoft.com/en-us/library/t32cf9tb.aspx - if (needed > -1) - { - _formatBuffer.resize(static_cast(needed) + 1); - - const auto written = _vsnprintf_s(_formatBuffer.data(), _formatBuffer.size(), needed, pFormat->c_str(), args); - hr = _Write({ _formatBuffer.data(), gsl::narrow(written) }); - } - else - { - hr = E_INVALIDARG; - } - - va_end(args); - - return hr; -} -CATCH_RETURN(); - // Method Description: // - This method will update the active font on the current device context // Does nothing for vt, the font is handed by the terminal. diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index a6d8e60f2..f7b0a788b 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -154,9 +154,18 @@ namespace Microsoft::Console::Render std::optional _newBottomLineBG{ std::nullopt }; [[nodiscard]] HRESULT _Write(std::string_view const str) noexcept; - [[nodiscard]] HRESULT _WriteFormattedString(const std::string* const pFormat, ...) noexcept; [[nodiscard]] HRESULT _Flush() noexcept; + template + [[nodiscard]] HRESULT _WriteFormatted(S&& format, Args&&... args) + try + { + fmt::basic_memory_buffer buf; + fmt::format_to(std::back_inserter(buf), std::forward(format), std::forward(args)...); + return _Write({ buf.data(), buf.size() }); + } + CATCH_RETURN() + void _OrRect(_Inout_ SMALL_RECT* const pRectExisting, const SMALL_RECT* const pRectToOr) const; bool _AllIsInvalid() const; diff --git a/src/server/ApiMessage.cpp b/src/server/ApiMessage.cpp index 4039a2a0c..598111c88 100644 --- a/src/server/ApiMessage.cpp +++ b/src/server/ApiMessage.cpp @@ -8,18 +8,51 @@ #include "ApiMessage.h" #include "DeviceComm.h" -_CONSOLE_API_MSG::_CONSOLE_API_MSG() : - Complete{ 0 }, - State{ 0 }, - _pDeviceComm(nullptr), - _pApiRoutines(nullptr), - _inputBuffer{}, - _outputBuffer{}, - Descriptor{ 0 }, - CreateObject{ 0 }, - CreateScreenBuffer{ 0 }, - msgHeader{ 0 } +constexpr size_t structPacketDataSize = sizeof(_CONSOLE_API_MSG) - offsetof(_CONSOLE_API_MSG, Descriptor); + +_CONSOLE_API_MSG::_CONSOLE_API_MSG() { + // A union cannot have more than one initializer, + // but it isn't exactly clear which union case is the largest. + // --> Just memset() the entire thing. + memset(&Descriptor, 0, structPacketDataSize); +} + +_CONSOLE_API_MSG::_CONSOLE_API_MSG(const _CONSOLE_API_MSG& other) +{ + *this = other; +} + +_CONSOLE_API_MSG& _CONSOLE_API_MSG::operator=(const _CONSOLE_API_MSG& other) +{ + Complete = other.Complete; + State = other.State; + _pDeviceComm = other._pDeviceComm; + _pApiRoutines = other._pApiRoutines; + _inputBuffer = other._inputBuffer; + _outputBuffer = other._outputBuffer; + + // Since this struct uses anonymous unions and thus cannot + // explicitly reference it, we have to a bit cheeky to copy it. + // --> Just memcpy() the entire thing. + memcpy(&Descriptor, &other.Descriptor, structPacketDataSize); + + if (State.InputBuffer) + { + State.InputBuffer = _inputBuffer.data(); + } + + if (State.OutputBuffer) + { + State.OutputBuffer = _outputBuffer.data(); + } + + if (Complete.Write.Data) + { + Complete.Write.Data = &u; + } + + return *this; } ConsoleProcessHandle* _CONSOLE_API_MSG::GetProcessHandle() const @@ -194,22 +227,3 @@ void _CONSOLE_API_MSG::SetReplyInformation(const ULONG_PTR pInformation) { Complete.IoStatus.Information = pInformation; } - -void _CONSOLE_API_MSG::UpdateUserBufferPointers() -{ - // There are some instances where an API message may get copied. - // Because it is infeasible to write a copy constructor for this class - // without rewriting large swaths of conhost (because of the unnamed union) - // we have chosen to introduce a "post-copy" step. - // This makes sure the buffers in State are in sync with the actual - // buffers in the object. - if (State.InputBuffer) - { - State.InputBuffer = _inputBuffer.data(); - } - - if (State.OutputBuffer) - { - State.OutputBuffer = _outputBuffer.data(); - } -} diff --git a/src/server/ApiMessage.h b/src/server/ApiMessage.h index 828d6358a..5b543620e 100644 --- a/src/server/ApiMessage.h +++ b/src/server/ApiMessage.h @@ -29,17 +29,42 @@ typedef struct _CONSOLE_API_MSG { _CONSOLE_API_MSG(); - CD_IO_COMPLETE Complete; - CONSOLE_API_STATE State; + _CONSOLE_API_MSG(_CONSOLE_API_MSG&&) = delete; + _CONSOLE_API_MSG& operator=(_CONSOLE_API_MSG&&) = delete; - IDeviceComm* _pDeviceComm; - IApiRoutines* _pApiRoutines; + _CONSOLE_API_MSG(const _CONSOLE_API_MSG& other); + _CONSOLE_API_MSG& operator=(const _CONSOLE_API_MSG&); + + ConsoleProcessHandle* GetProcessHandle() const; + ConsoleHandleData* GetObjectHandle() const; + + [[nodiscard]] HRESULT ReadMessageInput(const ULONG cbOffset, _Out_writes_bytes_(cbSize) PVOID pvBuffer, const ULONG cbSize); + [[nodiscard]] HRESULT GetAugmentedOutputBuffer(const ULONG cbFactor, + _Outptr_result_bytebuffer_(*pcbSize) PVOID* ppvBuffer, + _Out_ PULONG pcbSize); + [[nodiscard]] HRESULT GetOutputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); + [[nodiscard]] HRESULT GetInputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); + + [[nodiscard]] HRESULT ReleaseMessageBuffers(); + + void SetReplyStatus(const NTSTATUS Status); + void SetReplyInformation(const ULONG_PTR pInformation); + + // DO NOT PUT ACCESS SPECIFIERS HERE. + // The tail end of this structure is overwritten with console driver packet. + // It's important that we have a deterministic, C-like field ordering + // as this ensures that the packet data fields are last. + // Putting access specifiers here ("private:") would break this. + + CD_IO_COMPLETE Complete{}; + CONSOLE_API_STATE State{}; + + IDeviceComm* _pDeviceComm{ nullptr }; + IApiRoutines* _pApiRoutines{ nullptr }; -private: boost::container::small_vector _inputBuffer; boost::container::small_vector _outputBuffer; -public: // From here down is the actual packet data sent/received. CD_IO_DESCRIPTOR Descriptor; union @@ -62,33 +87,6 @@ public: }; // End packet data -public: - // DO NOT PUT MORE FIELDS DOWN HERE. - // The tail end of this structure will have a console driver packet - // copied over it and it will overwrite any fields declared here. - - ConsoleProcessHandle* GetProcessHandle() const; - ConsoleHandleData* GetObjectHandle() const; - - [[nodiscard]] HRESULT ReadMessageInput(const ULONG cbOffset, _Out_writes_bytes_(cbSize) PVOID pvBuffer, const ULONG cbSize); - [[nodiscard]] HRESULT GetAugmentedOutputBuffer(const ULONG cbFactor, - _Outptr_result_bytebuffer_(*pcbSize) PVOID* ppvBuffer, - _Out_ PULONG pcbSize); - [[nodiscard]] HRESULT GetOutputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); - [[nodiscard]] HRESULT GetInputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); - - [[nodiscard]] HRESULT ReleaseMessageBuffers(); - - void SetReplyStatus(const NTSTATUS Status); - void SetReplyInformation(const ULONG_PTR pInformation); - - // MSFT-33127449, GH#9692 - // We are not writing a copy constructor for this class - // so as to scope a fix as narrowly as possible to the - // "invalid user buffer" crash. - // TODO GH#10076: remove this. - void UpdateUserBufferPointers(); - // DO NOT PUT MORE FIELDS DOWN HERE. // The tail end of this structure will have a console driver packet // copied over it and it will overwrite any fields declared here. diff --git a/src/server/IoDispatchers.cpp b/src/server/IoDispatchers.cpp index c6009e9d4..1d76c6d4c 100644 --- a/src/server/IoDispatchers.cpp +++ b/src/server/IoDispatchers.cpp @@ -14,6 +14,7 @@ #include "../host/srvinit.h" #include "../host/telemetry.hpp" +#include "../interactivity/base/HostSignalInputThread.hpp" #include "../interactivity/inc/ServiceLocator.hpp" #include "../types/inc/utils.hpp" @@ -146,7 +147,7 @@ static bool _shouldAttemptHandoff(const Globals& globals, const CONSOLE_INFORMATION& gci, CONSOLE_API_CONNECTINFO& cac) { -#ifndef __INSIDE_WINDOWS +#if !TIL_FEATURE_ATTEMPTHANDOFF_ENABLED UNREFERENCED_PARAMETER(globals); UNREFERENCED_PARAMETER(gci); @@ -296,16 +297,50 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API HANDLE serverHandle; THROW_IF_FAILED(Globals.pDeviceComm->GetServerHandle(&serverHandle)); + wil::unique_hfile signalPipeTheirSide; + wil::unique_hfile signalPipeOurSide; + + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(signalPipeOurSide.addressof(), signalPipeTheirSide.addressof(), nullptr, 0)); + + // Give a copy of our own process handle to be tracked. + wil::unique_process_handle ourProcess; + THROW_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), + GetCurrentProcess(), + GetCurrentProcess(), + ourProcess.addressof(), + SYNCHRONIZE, + FALSE, + 0)); + + wil::unique_process_handle clientProcess; + // Okay, moment of truth! If they say they successfully took it over, we're going to clean up. // If they fail, we'll throw here and it'll log and we'll just start normally. THROW_IF_FAILED(handoff->EstablishHandoff(serverHandle, Globals.hInputEvent.get(), - &msg)); + &msg, + signalPipeTheirSide.get(), + ourProcess.get(), + &clientProcess)); + + // Close handles for the things we gave to them + signalPipeTheirSide.reset(); + ourProcess.reset(); + Globals.hInputEvent.reset(); + + // Start a thread to listen for signals from their side that we must relay to the OS. + auto hostSignalThread = std::make_unique(std::move(signalPipeOurSide)); + + // Start it if it was successfully created. + THROW_IF_FAILED(hostSignalThread->Start()); // Unlock in case anything tries to spool down as we exit. UnlockConsole(); - // We've handed off responsibility. Exit process to clean up any outstanding things we have open. + // We've handed off responsibility. Wait for child process to exit so we can maintain PID continuity for some clients. + WaitForSingleObject(clientProcess.get(), INFINITE); + + // Exit process to clean up any outstanding things we have open. ExitProcess(S_OK); } CATCH_LOG(); // Just log, don't do anything more. We'll move on to launching normally on failure. diff --git a/src/server/WaitBlock.cpp b/src/server/WaitBlock.cpp index 100055f88..b3318d418 100644 --- a/src/server/WaitBlock.cpp +++ b/src/server/WaitBlock.cpp @@ -28,10 +28,9 @@ ConsoleWaitBlock::ConsoleWaitBlock(_In_ ConsoleWaitQueue* const pProcessQueue, _In_ IWaitRoutine* const pWaiter) : _pProcessQueue(THROW_HR_IF_NULL(E_INVALIDARG, pProcessQueue)), _pObjectQueue(THROW_HR_IF_NULL(E_INVALIDARG, pObjectQueue)), + _WaitReplyMessage(*pWaitReplyMessage), _pWaiter(THROW_HR_IF_NULL(E_INVALIDARG, pWaiter)) { - _WaitReplyMessage = *pWaitReplyMessage; - // MSFT-33127449, GH#9692 // Until there's a "Wait", there's only one API message inflight at a time. In our // quest for performance, we put that single API message in charge of its own @@ -51,14 +50,8 @@ ConsoleWaitBlock::ConsoleWaitBlock(_In_ ConsoleWaitQueue* const pProcessQueue, // waiting message or the "wait completer" has a bunch of dangling pointers in it. // Oops. // - // Here, we fix up the message's internal pointers (in lieu of giving it a proper - // copy constructor; see GH#10076) and then tell the wait completion routine (which - // is going to be a COOKED_READ, RAW_READ, DirectRead or WriteData) about the new - // buffer location. - // - // This is a scoped fix that should be replaced (TODO GH#10076) with a final one - // after Ask mode. - _WaitReplyMessage.UpdateUserBufferPointers(); + // Here, we tell the wait completion routine (which is going to be a COOKED_READ, + // RAW_READ, DirectRead or WriteData) about the new buffer location. if (pWaitReplyMessage->State.InputBuffer) { @@ -69,12 +62,6 @@ ConsoleWaitBlock::ConsoleWaitBlock(_In_ ConsoleWaitQueue* const pProcessQueue, { _pWaiter->MigrateUserBuffersOnTransitionToBackgroundWait(pWaitReplyMessage->State.OutputBuffer, _WaitReplyMessage.State.OutputBuffer); } - - // We will write the original message back (with updated out parameters/payload) when the request is finally serviced. - if (pWaitReplyMessage->Complete.Write.Data != nullptr) - { - _WaitReplyMessage.Complete.Write.Data = &_WaitReplyMessage.u; - } } // Routine Description: @@ -85,11 +72,7 @@ ConsoleWaitBlock::~ConsoleWaitBlock() { _pProcessQueue->_blocks.erase(_itProcessQueue); _pObjectQueue->_blocks.erase(_itObjectQueue); - - if (_pWaiter != nullptr) - { - delete _pWaiter; - } + delete _pWaiter; } // Routine Description: diff --git a/src/staging/makefile.inc b/src/staging/makefile.inc new file mode 100644 index 000000000..cd46d2866 --- /dev/null +++ b/src/staging/makefile.inc @@ -0,0 +1,3 @@ +$(OBJ_PATH)\$(O)\TilFeatureStaging.h: ..\features.xml + set BUILDMSG=Building til::feature flag header... + powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy ByPass -Command ..\..\tools\Generate-FeatureStagingHeader.ps1 "$**" -Branding WindowsInbox -Branch $(BUILDBRANCH) -OutputPath "$@" diff --git a/src/staging/sources b/src/staging/sources new file mode 100644 index 000000000..f0390360c --- /dev/null +++ b/src/staging/sources @@ -0,0 +1,7 @@ +_NEED_BUILDBRANCH=1 +TARGETTYPE=NOTARGET + +NTTARGETFILE0=\ + $(OBJ_PATH)\$(O)\TilFeatureStaging.h + +# See build rules in makefile.inc diff --git a/src/til/ut_til/VisualizeControlCodesTests.cpp b/src/til/ut_til/VisualizeControlCodesTests.cpp deleted file mode 100644 index 4d492e924..000000000 --- a/src/til/ut_til/VisualizeControlCodesTests.cpp +++ /dev/null @@ -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)); - } -}; diff --git a/src/til/ut_til/mutex.cpp b/src/til/ut_til/mutex.cpp new file mode 100644 index 000000000..d316801dd --- /dev/null +++ b/src/til/ut_til/mutex.cpp @@ -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 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(); + } +}; diff --git a/src/til/ut_til/string.cpp b/src/til/ut_til/string.cpp new file mode 100644 index 000000000..3ea9eb3e4 --- /dev/null +++ b/src/til/ut_til/string.cpp @@ -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")); + } +}; diff --git a/src/til/ut_til/throttled_func.cpp b/src/til/ut_til/throttled_func.cpp new file mode 100644 index 000000000..3cb85025f --- /dev/null +++ b/src/til/ut_til/throttled_func.cpp @@ -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; + + til::latch latch{ 2 }; + + std::unique_ptr tf; + tf = std::make_unique(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(); + } +}; diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index d1fed1491..45e812076 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -10,24 +10,26 @@ - - - - - - - - - - - - - - Create + + + + + + + + + + + + + + + + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index 924ee6383..92003b075 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -4,19 +4,25 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/tools/FeatureStagingSchema.xsd b/tools/FeatureStagingSchema.xsd new file mode 100644 index 000000000..3e6819244 --- /dev/null +++ b/tools/FeatureStagingSchema.xsd @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/Generate-FeatureStagingHeader.ps1 b/tools/Generate-FeatureStagingHeader.ps1 new file mode 100644 index 000000000..8206b7bb1 --- /dev/null +++ b/tools/Generate-FeatureStagingHeader.ps1 @@ -0,0 +1,206 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +################################################################################ +# This script generates a header describing which Terminal/Console features +# should be compiled-in, based on an XML document describing them. + +[CmdletBinding()] +Param( + [Parameter(Position=0, Mandatory=$True)] + [ValidateScript({ Test-Path $_ })] + [string]$Path, + + [ValidateSet("Dev", "Preview", "Release", "WindowsInbox")] + [string]$Branding = "Dev", + + [string]$BranchOverride = $Null, + + [string]$OutputPath +) + +Enum Stage { + AlwaysDisabled; + AlwaysEnabled; +} + +Function ConvertTo-FeatureStage([string]$stage) { + Switch($stage) { + "AlwaysEnabled" { [Stage]::AlwaysEnabled; Return } + "AlwaysDisabled" { [Stage]::AlwaysDisabled; Return } + } + Throw "Invalid feature stage $stage" +} + +Class Feature { + [string]$Name + [Stage]$Stage + [System.Collections.Generic.Dictionary[string, Stage]]$BranchTokenStages + [System.Collections.Generic.Dictionary[string, Stage]]$BrandingTokenStages + [bool]$DisabledReleaseToken + + Feature([System.Xml.XmlElement]$entry) { + $this.Name = $entry.name + $this.Stage = ConvertTo-FeatureStage $entry.stage + $this.BranchTokenStages = [System.Collections.Generic.Dictionary[string, Stage]]::new() + $this.BrandingTokenStages = [System.Collections.Generic.Dictionary[string, Stage]]::new() + $this.DisabledReleaseToken = $Null -Ne $entry.alwaysDisabledReleaseTokens + + ForEach ($b in $entry.alwaysDisabledBranchTokens.branchToken) { + $this.BranchTokenStages[$b] = [Stage]::AlwaysDisabled + } + + # AlwaysEnabled branches win over AlwaysDisabled branches + ForEach ($b in $entry.alwaysEnabledBranchTokens.branchToken) { + $this.BranchTokenStages[$b] = [Stage]::AlwaysEnabled + } + + ForEach ($b in $entry.alwaysDisabledBrandingTokens.brandingToken) { + $this.BrandingTokenStages[$b] = [Stage]::AlwaysDisabled + } + + # AlwaysEnabled brandings win over AlwaysDisabled brandings + ForEach ($b in $entry.alwaysEnabledBrandingTokens.brandingToken) { + $this.BrandingTokenStages[$b] = [Stage]::AlwaysEnabled + } + } + + [string] PreprocessorName() { + return "TIL_$($this.Name.ToUpper())_ENABLED" + } +} + +class FeatureComparer : System.Collections.Generic.IComparer[Feature] { + [int] Compare([Feature]$a, [Feature]$b) { + If ($a.Name -lt $b.Name) { + Return -1 + } ElseIf ($a.Name -gt $b.Name) { + Return 1 + } Else { + Return 0 + } + } +} + +Function Resolve-FinalFeatureStage { + Param( + [Feature]$Feature, + [string]$Branch, + [string]$Branding + ) + + # RELEASE=DISABLED wins all checks + # Then, branch match by most-specific branch + # Then, branding type (if no overriding branch match) + + If ($Branding -Eq "Release" -And $Feature.DisabledReleaseToken) { + [Stage]::AlwaysDisabled + Return + } + + If (-Not [String]::IsNullOrEmpty($Branch)) { + $lastMatchLen = 0 + $branchStage = $Null + ForEach ($branchToken in $Feature.BranchTokenStages.Keys) { + # Match the longest branch token -- it should be the most specific + If ($Branch -Like $branchToken -And $branchToken.Length -Gt $lastMatchLen) { + $lastMatchLen = $branchToken.Length + $branchStage = $Feature.BranchTokenStages[$branchToken] + } + } + If ($Null -Ne $branchStage) { + $branchStage + Return + } + } + + $BrandingStage = $Feature.BrandingTokenStages[$Branding] + If ($Null -Ne $BrandingStage) { + $BrandingStage + Return + } + + $Feature.Stage +} + +$ErrorActionPreference = "Stop" +$x = [xml](Get-Content $Path -EA:Stop) +$x.Schemas.Add('http://microsoft.com/TilFeatureStaging-Schema.xsd', (Resolve-Path (Join-Path $PSScriptRoot "FeatureStagingSchema.xsd")).Path) | Out-Null +$x.Validate($null) + +$featureComparer = [FeatureComparer]::new() +$features = [System.Collections.Generic.List[Feature]]::new(16) + +ForEach ($entry in $x.featureStaging.feature) { + $features.Add([Feature]::new($entry)) +} + +$features.Sort($featureComparer) + +$featureFinalStages = [System.Collections.Generic.Dictionary[string, Stage]]::new(16) + +$branch = $BranchOverride +If ([String]::IsNullOrEmpty($branch)) { + Try { + $branch = & git branch --show-current 2>$Null + } Catch { + Try { + $branch = & git rev-parse --abbrev-ref HEAD 2>$Null + } Catch { + Write-Verbose "Cannot determine current Git branch; skipping branch validation" + } + } +} + +ForEach ($feature in $features) { + $featureFinalStages[$feature.Name] = Resolve-FinalFeatureStage -Feature $feature -Branch $branch -Branding $Branding +} + +### CODE GENERATION + +$script:Output = "" +Function AddOutput($s) { + $script:Output += $s +} + +AddOutput @" +// THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT IT +// INPUT FILE: $Path + +"@ + +ForEach ($feature in $features) { + $stage = $featureFinalStages[$feature.Name] + + AddOutput @" +#define $($feature.PreprocessorName()) $(If ($stage -eq [Stage]::AlwaysEnabled) { "1" } Else { "0" }) + +"@ +} + +AddOutput @" + +#if defined(__cplusplus) + +"@ + +ForEach ($feature in $features) { + AddOutput @" +__pragma(detect_mismatch("ODR_violation_$($feature.PreprocessorName())_mismatch", "$($feature.Stage)")) +struct $($feature.Name) +{ + static constexpr bool IsEnabled() { return $($feature.PreprocessorName()) == 1; } +}; + +"@ +} + +AddOutput @" +#endif +"@ + +If ([String]::IsNullOrEmpty($OutputPath)) { + $script:Output +} Else { + Out-File -Encoding UTF8 -FilePath $OutputPath -InputObject $script:Output +} diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index bb746507f..b1a8de52e 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -193,7 +193,7 @@ function Invoke-OpenConsoleTests() return } $OpenConsolePlatform = $Platform - $TestHostAppPath = "$root\$OpenConsolePlatform\$Configuration\TestHostApp" + $TestHostAppPath = "$root\bin\$OpenConsolePlatform\$Configuration\TestHostApp" if ($Platform -eq 'x86') { $OpenConsolePlatform = 'Win32' @@ -240,8 +240,10 @@ function Invoke-OpenConsoleTests() { & $TaefExePath "$TestHostAppPath\$($t.binary)" $TaefArgs } - - & $TaefExePath "$BinDir\$($t.binary)" $TaefArgs + else + { + & $TaefExePath "$BinDir\$($t.binary)" $TaefArgs + } } elseif ($t.type -eq "ft") { @@ -393,10 +395,10 @@ function Invoke-XamlFormat() { dotnet tool run xstyler -- -c "$root\XamlStyler.json" -f "$xamlsForStyler" # Strip BOMs from all the .xaml files - $xamls = (git ls-files "$root/**/*.xaml") - foreach ($file in $xamls ) { - $content = Get-Content $file - [IO.File]::WriteAllLines("$file", $content) + $xamls = (git ls-files --full-name "$root/**/*.xaml") + foreach ($file in $xamls) { + $content = Get-Content "$root/$file" + [IO.File]::WriteAllLines("$root/$file", $content) } } diff --git a/tools/tests.xml b/tools/tests.xml index 19507741d..fd6444662 100644 --- a/tools/tests.xml +++ b/tools/tests.xml @@ -6,8 +6,8 @@ - - + +