diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 9b7a8417b..e7ffdca52 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,10 +1,13 @@ -Apc apc +calt +ccmp +Apc clickable +clig copyable dalet -Dcs dcs +Dcs dialytika dje downside @@ -12,8 +15,10 @@ downsides dze dzhe Enum'd +Fitt formattings ftp +fvar geeksforgeeks ghe gje @@ -27,7 +32,10 @@ It'd kje liga lje +locl +lorem maxed +mkmk mru nje ogonek @@ -37,10 +45,12 @@ postmodern ptys qof qps +rclt reimplementation reserialization reserialize reserializes +rlig runtimes shcha slnt @@ -50,8 +60,10 @@ TLDR tokenizes tonos tshe +uiatextrange UIs und +unregister versioned We'd wildcards diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 67ef96545..696f9da66 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -2,11 +2,13 @@ ACCEPTFILES ACCESSDENIED alignas alignof +APPLYTOSUBMENUS bitfield bitfields BUILDBRANCH BUILDMSG BUILDNUMBER +BYPOSITION charconv CLASSNOTAVAILABLE cmdletbinding @@ -63,6 +65,7 @@ IObject iosfwd IPackage IPeasant +isspace IStorage istream IStringable @@ -77,7 +80,11 @@ llu localtime lround LSHIFT +MENUCOMMAND +MENUDATA +MENUINFO memicmp +mptt mov msappx MULTIPLEUSE @@ -92,6 +99,7 @@ NOCHANGEDIR NOPROGRESS NOREDIRECTIONBITMAP NOREPEAT +NOTIFYBYPOS NOTIFYICON NOTIFYICONDATA ntprivapi @@ -132,11 +140,14 @@ SRWLOCK STDCPP STDMETHOD strchr +strcpy streambuf +strtoul Stubless Subheader Subpage syscall +TASKBARCREATED TBPF THEMECHANGED tlg @@ -151,6 +162,7 @@ userenv wcsstr wcstoui winmain +wmemcmp wpc wsregex wwinmain diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index ca1b1cadb..87d7a3d8c 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -25,6 +25,7 @@ DWINRT enablewttlogging Intelli LKG +Lxss mfcribbon microsoft microsoftonline diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index b4fe3abae..27ba53635 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -31,6 +31,7 @@ Kourosh kowalczyk leonmsft Lepilleur +lhecker lukesampson Manandhar mbadolato @@ -66,6 +67,7 @@ sonpham stakx thereses Walisch +Wellons Wirt Wojciech zadjii diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 371dc9c62..8ed94f9d4 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -105,6 +105,7 @@ autoscrolling Autowrap AVerify AVI +AVX awch azuredevopspodcast azzle @@ -168,6 +169,7 @@ brandings BRK Browsable bsearch +Bspace bstr BTNFACE buf @@ -190,6 +192,7 @@ CARETBLINKINGENABLED CARRIAGERETURN cascadia cassert +castsi catid cazamor CBash @@ -268,9 +271,12 @@ cmder CMDEXT Cmdlet cmdline +cmh CMOUSEBUTTONS cmp +cmpeq cmt +cmw cmyk CNL cnt @@ -403,11 +409,13 @@ csbiex csharp CSHORT CSIDL +Cspace csproj Csr csrmsg CSRSS csrutil +css cstdarg cstddef cstdio @@ -506,6 +514,8 @@ DECAWM DECCKM DECCOLM DECDHL +decdld +DECDLD DECDWL DECEKBD DECID @@ -778,6 +788,7 @@ flyout fmodern fmtarg fmtid +FNV FOLDERID FONTCHANGE fontdlg @@ -786,6 +797,7 @@ FONTENUMPROC FONTFACE FONTFAMILY FONTHEIGHT +FONTINFO fontlist FONTOK FONTSIZE @@ -899,6 +911,7 @@ github gitlab gle globals +GLYPHENTRY gmail GMEM GNUC @@ -947,6 +960,7 @@ hdrstop HEIGHTSCROLL hfile hfont +hfontresource hglobal hhh HHmm @@ -1021,6 +1035,7 @@ IAction IApi IApplication IBase +ICache icacls iccex icch @@ -1260,6 +1275,7 @@ lnkd lnkfile LNM LOADONCALL +loadu LOBYTE localappdata localhost @@ -1267,6 +1283,7 @@ locsrc locstudio Loewen LOGFONT +LOGFONTA LOGFONTW logissue lowercased @@ -1420,6 +1437,7 @@ MOUSEFIRST MOUSEHWHEEL MOUSEMOVE mousewheel +movemask MOVESTART msb msbuild @@ -1452,6 +1470,7 @@ Mul multiline munged munges +murmurhash mutex mutexes muxes @@ -1502,6 +1521,7 @@ nfe nlength Nls NLSMODE +nnn NOACTIVATE NOAPPLYNOW NOCLIP @@ -1582,6 +1602,7 @@ NTVDM ntverp NTWIN nuget +nullability nullness nullonfailure nullopt @@ -1927,6 +1948,7 @@ realloc reamapping rects redef +redefinable Redir redirector redist @@ -1972,6 +1994,7 @@ rfc rftp rgb rgba +RGBCOLOR rgbi rgci rgfae @@ -2141,6 +2164,7 @@ SIGDN SINGLEFLAG SINGLETHREADED siup +sixel SIZEBOX sizeof SIZESCROLL @@ -2245,6 +2269,7 @@ SWMR SWP swprintf SYMED +symlink SYNCPAINT sys syscalls @@ -2528,6 +2553,7 @@ vcvarsall vcxitems vcxproj vec +vectorized VERCTRL versioning VERTBAR @@ -2745,6 +2771,7 @@ WTo wtof wtoi WTs +WTSOFTFONT wtw wtypes Wubi @@ -2774,6 +2801,7 @@ xes xff XFile XFORM +xIcon XManifest XMath XMFLOAT @@ -2806,6 +2834,7 @@ YCast YCENTER YCount YDPI +yIcon yml YOffset YPosition diff --git a/OpenConsole.sln b/OpenConsole.sln index 843d866cc..493cba041 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -73,6 +73,7 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host.unittest", "src\host\ut_lib\host.unittest.vcxproj", "{06EC74CB-9A12-429C-B551-8562EC954747}" ProjectSection(ProjectDependencies) = postProject {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} = {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host.Tests.Unit", "src\host\ut_host\Host.UnitTests.vcxproj", "{531C23E7-4B76-4C08-8AAD-04164CB628C9}" diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index 847846f83..e200db166 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -1,48 +1,487 @@ +# This build should never run as CI or against a pull request. trigger: none pr: none +pool: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest + +parameters: + - name: branding + displayName: "Branding (Build Type)" + type: string + default: Release + values: + - Release + - Preview + - name: buildTerminal + displayName: "Build Windows Terminal MSIX" + type: boolean + default: true + - name: buildTerminalVPack + displayName: "Build Windows Terminal VPack" + type: boolean + default: false + - name: buildWPF + displayName: "Build Terminal WPF Control" + type: boolean + default: false + - name: pgoBuildMode + displayName: "PGO Build Mode" + type: string + default: Optimize + values: + - Optimize + - Instrument + - None + + - name: buildConfigurations + type: object + default: + - Release + - name: buildPlatforms + type: object + default: + - x64 + - x86 + - arm64 + variables: - baseYearForVersioning: 2019 # Used by build-console-int - versionMajor: 0 - versionMinor: 1 + TerminalInternalPackageVersion: "0.0.7" -# When we move off PackageES for Versioning, we'll need to switch -# name to this format. For now, though, we need to use DayOfYear.Rev -# to unique our builds, as mandated by PackageES's Setup task. -# name: '$(versionMajor).$(versionMinor).$(DayOfYear)$(Rev:r).0' -# -# Build name/version number above must end with .0 to make the -# store publication machinery happy. -name: 'Terminal_$(date:yyMM).$(date:dd)$(rev:rrr)' - -# Build Arguments: -# WindowsTerminalOfficialBuild=[true,false] -# true - this is running on our build agent -# false - running locally -# WindowsTerminalBranding=[Dev,Preview,Release] -# - Development build resources (default) -# Preview - Preview build resources -# Release - regular build resources +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) +resources: + repositories: + - repository: self + type: git + ref: main jobs: - - template: ./templates/build-console-audit-job.yml - parameters: - platform: x64 +- job: Build + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ each platform in parameters.buildPlatforms }}: + ${{ config }}_${{ platform }}: + BuildConfiguration: ${{ config }} + BuildPlatform: ${{ platform }} + displayName: Build + cancelTimeoutInMinutes: 1 + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + - task: PowerShell@2 + displayName: Rationalize Build Platform + inputs: + targetType: inline + script: >- + $Arch = "$(BuildPlatform)" - - template: ./templates/build-console-int.yml - parameters: - platform: x64 - additionalBuildArguments: /p:WindowsTerminalOfficialBuild=true;WindowsTerminalBranding=Preview + If ($Arch -Eq "x86") { $Arch = "Win32" } - - template: ./templates/build-console-int.yml - parameters: - platform: x86 - additionalBuildArguments: /p:WindowsTerminalOfficialBuild=true;WindowsTerminalBranding=Preview + Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" + - task: NuGetToolInstaller@1 + displayName: Use NuGet 5.10 + inputs: + versionSpec: 5.10 + - task: NuGetCommand@2 + displayName: NuGet custom + inputs: + command: custom + selectOrConfig: config + nugetConfigPath: NuGet.Config + arguments: restore OpenConsole.sln -SolutionDirectory $(Build.SourcesDirectory) + - task: UniversalPackages@0 + displayName: Download terminal-internal Universal Package + inputs: + feedListDownload: 2b3f8893-a6e8-411f-b197-a9e05576da48 + packageListDownload: e82d490c-af86-4733-9dc4-07b772033204 + versionListDownload: $(TerminalInternalPackageVersion) + - task: TouchdownBuildTask@1 + displayName: Download Localization Files + inputs: + teamId: 7105 + authId: $(TouchdownAppId) + authKey: $(TouchdownAppKey) + resourceFilePath: >- + src\cascadia\TerminalApp\Resources\en-US\Resources.resw - - template: ./templates/build-console-int.yml - parameters: - platform: arm64 - additionalBuildArguments: /p:WindowsTerminalOfficialBuild=true;WindowsTerminalBranding=Preview + src\cascadia\TerminalControl\Resources\en-US\Resources.resw - - template: ./templates/check-formatting.yml + src\cascadia\TerminalConnection\Resources\en-US\Resources.resw - - template: ./templates/release-sign-and-bundle.yml + src\cascadia\TerminalSettingsModel\Resources\en-US\Resources.resw + + src\cascadia\TerminalSettingsEditor\Resources\en-US\Resources.resw + + src\cascadia\WindowsTerminalUniversal\Resources\en-US\Resources.resw + + src\cascadia\CascadiaPackage\Resources\en-US\Resources.resw + appendRelativeDir: true + localizationTarget: false + pseudoSetting: Included + - task: PowerShell@2 + displayName: Move Loc files one level up + inputs: + targetType: inline + script: >- + $Files = Get-ChildItem . -R -Filter 'Resources.resw' | ? FullName -Like '*en-US\*\Resources.resw' + + $Files | % { Move-Item -Verbose $_.Directory $_.Directory.Parent.Parent -EA:Ignore } + pwsh: true + - task: PowerShell@2 + displayName: Generate NOTICE.html from NOTICE.md + inputs: + filePath: .\build\scripts\Generate-ThirdPartyNotices.ps1 + arguments: -MarkdownNoticePath .\NOTICE.md -OutputPath .\src\cascadia\CascadiaPackage\NOTICE.html + pwsh: true + - ${{ if eq(parameters.pgoBuildMode, 'Optimize') }}: + - task: PowerShell@2 + displayName: Restore PGO Database + inputs: + filePath: tools/PGODatabase/restore-pgodb.ps1 + workingDirectory: $(Build.SourcesDirectory)\tools\PGODatabase + - ${{ if eq(parameters.buildTerminal, true) }}: + - task: VSBuild@1 + displayName: Build solution **\OpenConsole.sln + inputs: + solution: '**\OpenConsole.sln' + vsVersion: 16.0 + msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }} /t:Terminal\CascadiaPackage;Terminal\WindowsTerminalUniversal /p:WindowsTerminalReleaseBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: true + maximumCpuCount: true + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: binlog' + condition: failed() + continueOnError: True + inputs: + PathtoPublish: $(Build.SourcesDirectory)\msbuild.binlog + ArtifactName: binlog-$(BuildPlatform) + - ${{ if eq(parameters.pgoBuildMode, 'Optimize') }}: + - task: PowerShell@2 + displayName: Validate binaries are optimized + condition: and(succeeded(), eq(variables['BuildPlatform'], 'x64')) + inputs: + targetType: inline + script: >- + $Binaries = 'OpenConsole.exe', 'WindowsTerminal.exe', 'TerminalApp.dll', 'TerminalConnection.dll', 'Microsoft.Terminal.Control.dll', 'Microsoft.Terminal.Remoting.dll', 'Microsoft.Terminal.Settings.Editor.dll', 'Microsoft.Terminal.Settings.Model.dll' + + foreach ($BinFile in $Binaries) { + + & "$(Build.SourcesDirectory)\tools\PGODatabase\verify-pgo.ps1" "$(Build.SourcesDirectory)/src/cascadia/CascadiaPackage/bin/$(BuildPlatform)/$(BuildConfiguration)/$BinFile" + + } + - task: PowerShell@2 + displayName: Check MSIX for common regressions + inputs: + targetType: inline + script: >- + $Package = Get-ChildItem -Recurse -Filter "CascadiaPackage_*.msix" + + .\build\scripts\Test-WindowsTerminalPackage.ps1 -Verbose -Path $Package.FullName + pwsh: true + - ${{ if eq(parameters.buildWPF, true) }}: + - task: VSBuild@1 + displayName: Build solution **\OpenConsole.sln for PublicTerminalCore + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) + inputs: + solution: '**\OpenConsole.sln' + vsVersion: 16.0 + msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }} /p:WindowsTerminalReleaseBuild=true /t:Terminal\wpf\PublicTerminalCore + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + - task: PowerShell@2 + displayName: Source Index PDBs + inputs: + filePath: build\scripts\Index-Pdbs.ps1 + arguments: -SearchDir '$(Build.SourcesDirectory)' -SourceRoot '$(Build.SourcesDirectory)' -recursive -Verbose -CommitId $(Build.SourceVersion) + errorActionPreference: silentlyContinue + - task: ComponentGovernanceComponentDetection@0 + displayName: Component Detection + - task: PowerShell@2 + displayName: Run Unit Tests + condition: and(succeeded(), or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) + enabled: False + inputs: + filePath: build\scripts\Run-Tests.ps1 + arguments: -MatchPattern '*unit.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' + - task: PowerShell@2 + displayName: Run Feature Tests + condition: and(succeeded(), eq(variables['BuildPlatform'], 'x64')) + enabled: False + inputs: + filePath: build\scripts\Run-Tests.ps1 + arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' + - ${{ if eq(parameters.buildTerminal, true) }}: + - task: CopyFiles@2 + displayName: Copy *.appx/*.msix to Artifacts + inputs: + Contents: >- + **/*.appx + + **/*.msix + + **/*.appxsym + + !**/Microsoft.VCLibs*.appx + TargetFolder: $(Build.ArtifactStagingDirectory)/appx + OverWrite: true + flattenFolders: true + - task: PublishBuildArtifacts@1 + displayName: Publish Artifact (appx) + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory)/appx + ArtifactName: appx-$(BuildPlatform)-$(BuildConfiguration) + - ${{ if eq(parameters.buildWPF, true) }}: + - task: CopyFiles@2 + displayName: Copy PublicTerminalCore.dll to Artifacts + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) + inputs: + Contents: >- + **/PublicTerminalCore.dll + + **/api-ms-win-core-synch-l1-2-0.dll + TargetFolder: $(Build.ArtifactStagingDirectory)/wpf + OverWrite: true + flattenFolders: true + - task: PublishBuildArtifacts@1 + displayName: Publish Artifact (PublicTerminalCore) + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory)/wpf + ArtifactName: wpf-dll-$(BuildPlatform)-$(BuildConfiguration) + - task: PublishSymbols@2 + displayName: Publish symbols path + continueOnError: True + inputs: + SearchPattern: '**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + +- ${{ if eq(parameters.buildTerminal, true) }}: + - job: BundleAndSign + displayName: Create and sign AppX/MSIX bundles + dependsOn: Build + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts (*.appx, *.msix) + inputs: + downloadType: specific + itemPattern: >- + **/*.msix + + **/*.appx + extractTars: false + - task: PowerShell@2 + displayName: Create WindowsTerminal*.msixbundle + inputs: + filePath: build\scripts\Create-AppxBundle.ps1 + arguments: -InputPath "$(System.ArtifactsDirectory)" -ProjectName CascadiaPackage -BundleVersion 0.0.0.0 -OutputPath "$(System.ArtifactsDirectory)\Microsoft.WindowsTerminal_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + - task: PowerShell@2 + displayName: Create WindowsTerminalUniversal*.msixbundle + inputs: + filePath: build\scripts\Create-AppxBundle.ps1 + arguments: -InputPath "$(System.ArtifactsDirectory)" -ProjectName WindowsTerminalUniversal -BundleVersion $(XES_APPXMANIFESTVERSION) -OutputPath "$(System.ArtifactsDirectory)\Microsoft.WindowsTerminalUniversal_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + - task: EsrpCodeSigning@1 + displayName: Submit *.msixbundle to ESRP for code signing + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: $(System.ArtifactsDirectory) + Pattern: Microsoft.WindowsTerminal*.msixbundle + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: appxbundle-signed' + inputs: + PathtoPublish: $(System.ArtifactsDirectory) + ArtifactName: appxbundle-signed + +- ${{ if eq(parameters.buildWPF, true) }}: + - job: PackageAndSignWPF + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ config }}: + BuildConfiguration: ${{ config }} + displayName: Create NuGet Package (WPF Terminal Control) + dependsOn: Build + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + - task: DownloadBuildArtifacts@0 + displayName: Download x86 PublicTerminalCore + inputs: + artifactName: wpf-dll-x86-$(BuildConfiguration) + itemPattern: '**/*.dll' + downloadPath: bin\Win32\$(BuildConfiguration)\ + extractTars: false + - task: DownloadBuildArtifacts@0 + displayName: Download x64 PublicTerminalCore + inputs: + artifactName: wpf-dll-x64-$(BuildConfiguration) + itemPattern: '**/*.dll' + downloadPath: bin\x64\$(BuildConfiguration)\ + extractTars: false + - task: PowerShell@2 + displayName: Move downloaded artifacts up a level + inputs: + targetType: inline + # Find all artifact files and move them up a directory. Ugh. + script: >- + Get-ChildItem bin -Recurse -Directory -Filter wpf-dll-* | % { + $_ | Get-ChildItem -Recurse -File | % { + Move-Item -Verbose $_.FullName $_.Directory.Parent.FullName + } + } + - task: NuGetToolInstaller@1 + displayName: Use NuGet 5.10.0 + inputs: + versionSpec: 5.10.0 + - task: NuGetCommand@2 + displayName: NuGet restore copy + inputs: + selectOrConfig: config + nugetConfigPath: NuGet.Config + - task: VSBuild@1 + displayName: Build solution **\OpenConsole.sln for WPF Control + inputs: + solution: '**\OpenConsole.sln' + vsVersion: 16.0 + msbuildArgs: /p:WindowsTerminalReleaseBuild=$(UseReleaseBranding);Version=$(XES_PACKAGEVERSIONNUMBER) /t:Pack + platform: Any CPU + configuration: $(BuildConfiguration) + maximumCpuCount: true + - task: PublishSymbols@2 + displayName: Publish symbols path + continueOnError: True + inputs: + SearchPattern: '**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + SymbolsArtifactName: Symbols_WPF_$(BuildConfiguration) + - task: CopyFiles@2 + displayName: Copy *.nupkg to Artifacts + inputs: + Contents: '**/*Wpf*.nupkg' + TargetFolder: $(Build.ArtifactStagingDirectory)/nupkg + OverWrite: true + flattenFolders: true + - task: EsrpCodeSigning@1 + displayName: Submit *.nupkg to ESRP for code signing + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: $(Build.ArtifactStagingDirectory)/nupkg + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + - task: PublishBuildArtifacts@1 + displayName: Publish Artifact (nupkg) + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory)\nupkg + ArtifactName: wpf-nupkg-$(BuildConfiguration) + +- ${{ if eq(parameters.buildTerminalVPack, true) }}: + - job: VPack + displayName: Create Windows vPack + dependsOn: BundleAndSign + steps: + - checkout: self + clean: true + submodules: true + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + - task: DownloadBuildArtifacts@0 + displayName: Download Build Artifacts + inputs: + artifactName: appxbundle-signed + extractTars: false + - task: PowerShell@2 + displayName: Rename and stage packages for vpack + inputs: + targetType: inline + script: >- + # Rename to known/fixed name for Windows build system + + Get-ChildItem Microsoft.WindowsTerminal_*.msixbundle | Rename-Item -NewName { 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle' } + + + # Create vpack directory and place item inside + + mkdir WindowsTerminal.app + + mv Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle .\WindowsTerminal.app\ + workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed + - task: PkgESVPack@12 + displayName: 'Package ES - VPack' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + sourceDirectory: $(System.ArtifactsDirectory)\appxbundle-signed\WindowsTerminal.app + description: Windows Terminal pre-install application + pushPkgName: WindowsTerminal.app + owner: condev +... diff --git a/build/pipelines/templates/build-console-audit-job.yml b/build/pipelines/templates/build-console-audit-job.yml index 1c9a90d6e..2f1283f34 100644 --- a/build/pipelines/templates/build-console-audit-job.yml +++ b/build/pipelines/templates/build-console-audit-job.yml @@ -8,9 +8,12 @@ jobs: variables: BuildConfiguration: AuditMode BuildPlatform: ${{ parameters.platform }} - pool: "windevbuildagents" - # The public pool is also an option! - # pool: { vmImage: windows-2019 } + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPoolOSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest steps: - checkout: self diff --git a/build/pipelines/templates/build-console-ci.yml b/build/pipelines/templates/build-console-ci.yml index 0b9ce8685..0ff8b6b54 100644 --- a/build/pipelines/templates/build-console-ci.yml +++ b/build/pipelines/templates/build-console-ci.yml @@ -11,9 +11,12 @@ jobs: variables: BuildConfiguration: ${{ parameters.configuration }} BuildPlatform: ${{ parameters.platform }} - pool: "windevbuildagents" - # The public pool is also an option! - # pool: { vmImage: windows-2019 } + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPoolOSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest steps: - template: build-console-steps.yml diff --git a/build/pipelines/templates/build-console-int.yml b/build/pipelines/templates/build-console-int.yml deleted file mode 100644 index cc051df3f..000000000 --- a/build/pipelines/templates/build-console-int.yml +++ /dev/null @@ -1,31 +0,0 @@ -parameters: - configuration: 'Release' - platform: '' - additionalBuildArguments: '' - -jobs: -- job: Build${{ parameters.platform }}${{ parameters.configuration }} - displayName: Build ${{ parameters.platform }} ${{ parameters.configuration }} - variables: - BuildConfiguration: ${{ parameters.configuration }} - BuildPlatform: ${{ parameters.platform }} - PGOBuildMode: 'Optimize' - - pool: - name: Package ES Lab E - demands: - - msbuild - - visualstudio - - vstest - - steps: - - task: PkgESSetupBuild@10 - displayName: 'Package ES - Setup Build' - inputs: - useDfs: false - productName: WindowsTerminal - disableOutputRedirect: true - - - template: build-console-steps.yml - parameters: - additionalBuildArguments: "/p:XesUseOneStoreVersioning=true;XesBaseYearForStoreVersion=$(baseYearForVersioning) ${{ parameters.additionalBuildArguments }}" diff --git a/build/pipelines/templates/build-console-pgo.yml b/build/pipelines/templates/build-console-pgo.yml index 8af4ec5d8..1e33e82c8 100644 --- a/build/pipelines/templates/build-console-pgo.yml +++ b/build/pipelines/templates/build-console-pgo.yml @@ -12,9 +12,12 @@ jobs: BuildConfiguration: ${{ parameters.configuration }} BuildPlatform: ${{ parameters.platform }} PGOBuildMode: 'Instrument' - pool: "windevbuildagents" - # The public pool is also an option! - # pool: { vmImage: windows-2019 } + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPoolOSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: WinDevPool-L + demands: ImageOverride -equals WinDevVS16-latest steps: - template: build-console-steps.yml diff --git a/build/pipelines/templates/release-sign-and-bundle.yml b/build/pipelines/templates/release-sign-and-bundle.yml deleted file mode 100644 index ced32a05f..000000000 --- a/build/pipelines/templates/release-sign-and-bundle.yml +++ /dev/null @@ -1,74 +0,0 @@ -parameters: - configuration: 'Release' - -jobs: -- job: SignDeploy${{ parameters.configuration }} - displayName: Sign and Deploy for ${{ parameters.configuration }} - - dependsOn: - - Buildx64AuditMode - - Buildx64Release - - Buildx86Release - - Buildarm64Release - - CodeFormatCheck - condition: | - and - ( - in(dependencies.Buildx64AuditMode.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.Buildx64Release.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.Buildx86Release.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.Buildarm64Release.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), - in(dependencies.CodeFormatCheck.result, 'Succeeded', 'SucceededWithIssues', 'Skipped') - ) - - variables: - BuildConfiguration: ${{ parameters.configuration }} - AppxProjectName: CascadiaPackage - AppxBundleName: Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle - - pool: - name: Package ES Lab E - - steps: - - checkout: self - clean: true - - - task: PkgESSetupBuild@10 - displayName: 'Package ES - Setup Build' - inputs: - useDfs: false - productName: WindowsTerminal - disableOutputRedirect: true - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: 'Component Detection' - - - task: DownloadBuildArtifacts@0 - displayName: Download AppX artifacts - inputs: - artifactName: 'appx-$(BuildConfiguration)' - itemPattern: | - **/*.appx - **/*.msix - downloadPath: '$(Build.ArtifactStagingDirectory)\appx' - - - task: PowerShell@2 - displayName: 'Create $(AppxBundleName)' - inputs: - targetType: filePath - filePath: '.\build\scripts\Create-AppxBundle.ps1' - arguments: | - -InputPath "$(Build.ArtifactStagingDirectory)\appx" -ProjectName $(AppxProjectName) -BundleVersion 0.0.0.0 -OutputPath "$(Build.ArtifactStagingDirectory)\$(AppxBundleName)" - - - task: PkgESCodeSign@10 - displayName: 'Package ES - SignConfig.WindowsTerminal.xml' - inputs: - signConfigXml: 'build\config\SignConfig.WindowsTerminal.xml' - inPathRoot: '$(Build.ArtifactStagingDirectory)' - outPathRoot: '$(Build.ArtifactStagingDirectory)\signed' - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Signed AppX' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)\signed' - ArtifactName: 'appxbundle-signed-$(BuildConfiguration)' diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 0294330e9..13e3a2b0c 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -4,13 +4,13 @@ "title": "Microsoft's Windows Terminal Settings Profile Schema", "definitions": { "KeyChordSegment": { - "pattern": "^(?(?ctrl|alt|shift|win)(?:\\+(?ctrl|alt|shift|win)(?))?(?:\\+(?ctrl|alt|shift|win)(?|\\k))?(?:\\+(?ctrl|alt|shift|win)(?|\\k|\\k))?\\+)?(?[^\\s+]|app|menu|backspace|tab|enter|esc|escape|space|pgup|pageup|pgdn|pagedown|end|home|left|up|right|down|insert|delete|(?\", where each modifier is optional, separated by + symbols, and keyName is either one of the names listed in the table below, or any single key character. The string should be written in full lowercase.\napp, menu\tMENU key\nbackspace\tBACKSPACE key\ntab\tTAB key\nenter\tENTER key\nesc, escape\tESC key\nspace\tSPACEBAR\npgup, pageup\tPAGE UP key\npgdn, pagedown\tPAGE DOWN key\nend\tEND key\nhome\tHOME key\nleft\tLEFT ARROW key\nup\tUP ARROW key\nright\tRIGHT ARROW key\ndown\tDOWN ARROW key\ninsert\tINS key\ndelete\tDEL key\nnumpad_0-numpad_9, numpad0-numpad9\tNumeric keypad keys 0 to 9. Can't be combined with the shift modifier.\nnumpad_multiply\tNumeric keypad MULTIPLY key (*)\nnumpad_plus, numpad_add\tNumeric keypad ADD key (+)\nnumpad_minus, numpad_subtract\tNumeric keypad SUBTRACT key (-)\nnumpad_period, numpad_decimal\tNumeric keypad DECIMAL key (.). Can't be combined with the shift modifier.\nnumpad_divide\tNumeric keypad DIVIDE key (/)\nf1-f24\tF1 to F24 function keys\nplus\tADD key (+)" + "description": "The string should fit the format \"[ctrl+][alt+][shift+][win+]\", where each modifier is optional. KeyName is either any single key character, an explicit virtual key or scan code in the form vk(nnn) and sc(nnn) respectively, or one of the special names listed at https://docs.microsoft.com/en-us/windows/terminal/customize-settings/actions#accepted-modifiers-and-keys" }, "Color": { "default": "#", - "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", + "pattern": "^#[A-Fa-f0-9]{3}(?:[A-Fa-f0-9]{3})?$", "type": "string", "format": "color" }, @@ -154,6 +154,17 @@ "description": "Sets how the background image aligns to the boundaries of the window when unfocused. Possible values: \"center\", \"left\", \"top\", \"right\", \"bottom\", \"topLeft\", \"topRight\", \"bottomLeft\", \"bottomRight\"", "type": "string" }, + "intenseTextStyle": { + "default": "bright", + "description": "Controls how 'intense' text is rendered. Values are \"bold\", \"bright\", \"all\" and \"none\"", + "enum": [ + "none", + "bold", + "bright", + "all" + ], + "type": "string" + }, "experimental.retroTerminalEffect": { "description": "When set to true, enable retro terminal effects when unfocused. This is an experimental feature, and its continued existence is not guaranteed.", "type": "boolean" @@ -238,6 +249,8 @@ "identifyWindow", "identifyWindows", "moveFocus", + "movePane", + "swapPane", "moveTab", "newTab", "newWindow", @@ -270,6 +283,7 @@ "toggleFocusMode", "toggleFullscreen", "togglePaneZoom", + "toggleSplitOrientation", "toggleReadOnlyMode", "toggleShaderEffects", "wt", @@ -283,7 +297,9 @@ "right", "up", "down", - "previous" + "previous", + "nextInOrder", + "previousInOrder" ], "type": "string" }, @@ -491,6 +507,23 @@ "type": "integer", "default": 0, "description": "Which tab to switch to, with the first being 0" + } + } + } + ], + "required": [ "index" ] + }, + "MovePaneAction": { + "description": "Arguments corresponding to a Move Pane Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "movePane" }, + "index": { + "type": "integer", + "default": 0, + "description": "Which tab to move the pane to, with the first being 0" } } } @@ -507,7 +540,24 @@ "direction": { "$ref": "#/definitions/FocusDirection", "default": "left", - "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane." + "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." + } + } + } + ], + "required": [ "direction" ] + }, + "SwapPaneAction": { + "description": "Arguments corresponding to a Swap Pane Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "swapPane" }, + "direction": { + "$ref": "#/definitions/FocusDirection", + "default": "left", + "description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." } } } @@ -596,7 +646,7 @@ "defaultsFile", "allFiles", "settingsUI" - + ] } } @@ -952,6 +1002,8 @@ { "$ref": "#/definitions/NewTabAction" }, { "$ref": "#/definitions/SwitchToTabAction" }, { "$ref": "#/definitions/MoveFocusAction" }, + { "$ref": "#/definitions/MovePaneAction" }, + { "$ref": "#/definitions/SwapPaneAction" }, { "$ref": "#/definitions/ResizePaneAction" }, { "$ref": "#/definitions/SendInputAction" }, { "$ref": "#/definitions/SplitPaneAction" }, @@ -1116,6 +1168,10 @@ "description": "When set to true, we will use the software renderer (a.k.a. WARP) instead of the hardware one.", "type": "boolean" }, + "experimental.input.forceVT": { + "description": "Force the terminal to use the legacy input encoding. Certain keys in some applications may stop working when enabling this setting.", + "type": "boolean" + }, "initialCols": { "default": 120, "description": "The number of columns displayed in the window upon first load. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), this property is ignored.", @@ -1158,6 +1214,16 @@ "minimum": 0, "type": [ "integer", "string" ], "deprecated": true + }, + "minimizeToTray": { + "default": "false", + "description": "When set to true, minimizing a Terminal window will no longer appear in the taskbar. Instead, a Terminal icon will appear in the system tray through which the user can access their windows.", + "type": "boolean" + }, + "alwaysShowTrayIcon": { + "default": "false", + "description": "When set to true, the Terminal's tray icon will always be shown in the system tray.", + "type": "boolean" }, "actions": { "description": "Properties are specific to each custom action.", diff --git a/scratch/ScratchIslandApp/Package/Package.wapproj b/scratch/ScratchIslandApp/Package/Package.wapproj index 262719541..5a9f07fbe 100644 --- a/scratch/ScratchIslandApp/Package/Package.wapproj +++ b/scratch/ScratchIslandApp/Package/Package.wapproj @@ -140,12 +140,12 @@ - + 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/SampleApp/MyPage.cpp b/scratch/ScratchIslandApp/SampleApp/MyPage.cpp index d4af3d80e..647fb11d2 100644 --- a/scratch/ScratchIslandApp/SampleApp/MyPage.cpp +++ b/scratch/ScratchIslandApp/SampleApp/MyPage.cpp @@ -5,7 +5,7 @@ #include "MyPage.h" #include #include "MyPage.g.cpp" -#include "..\..\..\src\cascadia\UnitTests_Control\MockControlSettings.h" +#include "MySettings.h" using namespace std::chrono_literals; using namespace winrt::Microsoft::Terminal; @@ -26,17 +26,24 @@ namespace winrt::SampleApp::implementation void MyPage::Create() { - TerminalConnection::EchoConnection conn{}; - auto settings = winrt::make_self(); + auto settings = winrt::make_self(); + auto connectionSettings{ TerminalConnection::ConptyConnection::CreateSettings(L"cmd.exe /k echo This TermControl is hosted in-proc...", + winrt::hstring{}, + L"", + nullptr, + 32, + 80, + winrt::guid()) }; + + // "Microsoft.Terminal.TerminalConnection.ConptyConnection" + winrt::hstring myClass{ winrt::name_of() }; + TerminalConnection::ConnectionInformation connectInfo{ myClass, connectionSettings }; + + TerminalConnection::ITerminalConnection conn{ TerminalConnection::ConnectionInformation::CreateConnection(connectInfo) }; 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: diff --git a/scratch/ScratchIslandApp/SampleApp/MyPage.xaml b/scratch/ScratchIslandApp/SampleApp/MyPage.xaml index f6e129ee7..0c132dea2 100644 --- a/scratch/ScratchIslandApp/SampleApp/MyPage.xaml +++ b/scratch/ScratchIslandApp/SampleApp/MyPage.xaml @@ -19,10 +19,15 @@ - + + + + + #include #include "MySettings.g.h" @@ -12,9 +14,6 @@ namespace winrt::SampleApp::implementation { struct MySettings : MySettingsT { - public: - MySettings() = default; - // --------------------------- Core Settings --------------------------- // All of these settings are defined in ICoreSettings. @@ -88,6 +87,14 @@ namespace winrt::SampleApp::implementation winrt::Microsoft::Terminal::Core::Color GetColorTableEntry(int32_t index) noexcept { return _ColorTable.at(index); } std::array ColorTable() { return _ColorTable; } void ColorTable(std::array /*colors*/) {} + + MySettings() + { + const auto campbellSpan = ::Microsoft::Console::Utils::CampbellColorTable(); + std::transform(campbellSpan.begin(), campbellSpan.end(), _ColorTable.begin(), [](auto&& color) { + return static_cast(til::color{ color }); + }); + } }; } diff --git a/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj b/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj index 54a6c5706..c3ec0682f 100644 --- a/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj +++ b/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj @@ -147,13 +147,13 @@ - + 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/SampleApp/dll/SampleApp.vcxproj b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj index d49f6c697..34dabcdf3 100644 --- a/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj +++ b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj @@ -48,6 +48,10 @@ true true + + + {18D09A24-8240-42D6-8CB6-236EEE820263} + @@ -76,13 +80,13 @@ - + 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/SampleApp/packages.config b/scratch/ScratchIslandApp/SampleApp/packages.config index a54ab2a0e..e3bd479b1 100644 --- a/scratch/ScratchIslandApp/SampleApp/packages.config +++ b/scratch/ScratchIslandApp/SampleApp/packages.config @@ -1,6 +1,6 @@  - + diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj index e8cb725b7..2f6db3ae2 100644 --- a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj @@ -120,14 +120,14 @@ - + 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/WindowExe/packages.config b/scratch/ScratchIslandApp/WindowExe/packages.config index 1159f12ea..445748b67 100644 --- a/scratch/ScratchIslandApp/WindowExe/packages.config +++ b/scratch/ScratchIslandApp/WindowExe/packages.config @@ -2,6 +2,6 @@ - + diff --git a/src/buffer/out/OutputCellView.hpp b/src/buffer/out/OutputCellView.hpp index 06c9b86af..c2a7a6e3f 100644 --- a/src/buffer/out/OutputCellView.hpp +++ b/src/buffer/out/OutputCellView.hpp @@ -6,7 +6,7 @@ Module Name: - OutputCellView.hpp Abstract: -- Read-only view into a single cell of data that someone is attempting to write into the output buffer. +- Read view into a single cell of data that someone is attempting to write into the output buffer. - This is done for performance reasons (avoid heap allocs and copies). Author: @@ -36,6 +36,21 @@ public: TextAttribute TextAttr() const noexcept; TextAttributeBehavior TextAttrBehavior() const noexcept; + void UpdateText(const std::wstring_view& view) noexcept + { + _view = view; + }; + + void UpdateDbcsAttribute(const DbcsAttribute& dbcsAttr) noexcept + { + _dbcsAttr = dbcsAttr; + } + + void UpdateTextAttribute(const TextAttribute& textAttr) noexcept + { + _textAttr = textAttr; + } + bool operator==(const OutputCellView& view) const noexcept; bool operator!=(const OutputCellView& view) const noexcept; diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index 2791bc352..c44724846 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -95,16 +95,18 @@ bool TextAttribute::IsLegacy() const noexcept // - defaultFgColor: the default foreground color rgb value. // - defaultBgColor: the default background color rgb value. // - reverseScreenMode: true if the screen mode is reversed. -// - blinkingIsFaint: true if blinking should be interpreted as faint. +// - blinkingIsFaint: true if blinking should be interpreted as faint. (defaults to false) +// - boldIsBright: true if "bold" should be interpreted as bright. (defaults to true) // Return Value: // - the foreground and background colors that should be displayed. -std::pair TextAttribute::CalculateRgbColors(const gsl::span colorTable, +std::pair TextAttribute::CalculateRgbColors(const std::array& colorTable, const COLORREF defaultFgColor, const COLORREF defaultBgColor, const bool reverseScreenMode, - const bool blinkingIsFaint) const noexcept + const bool blinkingIsFaint, + const bool boldIsBright) const noexcept { - auto fg = _foreground.GetColor(colorTable, defaultFgColor, IsBold()); + auto fg = _foreground.GetColor(colorTable, defaultFgColor, boldIsBright && IsBold()); auto bg = _background.GetColor(colorTable, defaultBgColor); if (IsFaint() || (IsBlinking() && blinkingIsFaint)) { diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index e99360da2..00f8a2266 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -64,11 +64,12 @@ public: static TextAttribute StripErroneousVT16VersionsOfLegacyDefaults(const TextAttribute& attribute) noexcept; WORD GetLegacyAttributes() const noexcept; - std::pair CalculateRgbColors(const gsl::span colorTable, + std::pair CalculateRgbColors(const std::array& colorTable, const COLORREF defaultFgColor, const COLORREF defaultBgColor, const bool reverseScreenMode = false, - const bool blinkingIsFaint = false) const noexcept; + const bool blinkingIsFaint = false, + const bool boldIsBright = true) const noexcept; bool IsLeadingByte() const noexcept; bool IsTrailingByte() const noexcept; diff --git a/src/buffer/out/TextColor.cpp b/src/buffer/out/TextColor.cpp index 204050229..dcd0a8df3 100644 --- a/src/buffer/out/TextColor.cpp +++ b/src/buffer/out/TextColor.cpp @@ -50,6 +50,9 @@ constexpr std::array Index256ToIndex16 = { // clang-format on +// We should only need 4B for TextColor. Any more than that is just waste. +static_assert(sizeof(TextColor) == 4); + bool TextColor::CanBeBrightened() const noexcept { return IsIndex16() || IsDefault(); @@ -138,15 +141,12 @@ void TextColor::SetDefault() noexcept // - brighten: if true, we'll brighten a dark color table index. // Return Value: // - a COLORREF containing the real value of this TextColor. -COLORREF TextColor::GetColor(gsl::span colorTable, - const COLORREF defaultColor, - bool brighten) const noexcept +COLORREF TextColor::GetColor(const std::array& colorTable, const COLORREF defaultColor, bool brighten) const noexcept { if (IsDefault()) { if (brighten) { - FAIL_FAST_IF(colorTable.size() < 16); // See MSFT:20266024 for context on this fix. // Additionally todo MSFT:20271956 to fix this better for 19H2+ // If we're a default color, check to see if the defaultColor exists @@ -156,6 +156,61 @@ COLORREF TextColor::GetColor(gsl::span colorTable, // (Settings::_DefaultForeground==INVALID_COLOR, and the index // from _wFillAttribute is being used instead.) // If we find a match, return instead the bright version of this color + + static_assert(sizeof(COLORREF) * 8 == 32, "The vectorized code broke. If you can't fix COLORREF, just remove the vectorized code."); + +#pragma warning(push) +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). +#ifdef __AVX2__ + // I wrote this vectorized code one day, because the sun was shining so nicely. + // There's no other reason for this to exist here, except for being pretty. + // This code implements the exact same for loop you can find below, but is ~3x faster. + // + // A brief explanation for people unfamiliar with vectorized instructions: + // Vectorized instructions, like "SSE" or "AVX", allow you to run + // common operations like additions, multiplications, comparisons, + // or bitwise operations concurrently on multiple values at once. + // + // We want to find the given defaultColor in the first 8 values of colorTable. + // Coincidentally a COLORREF is a DWORD and 8 of them are exactly 256 bits. + // -- The size of a single AVX register. + // + // Thus, the code works like this: + // 1. Load all 8 DWORDs at once into one register + // 2. Set the same defaultColor 8 times in another register + // 3. Compare all 8 values at once + // The result is either 0xffffffff or 0x00000000. + // 4. Extract the most significant bit of each DWORD + // Assuming that no duplicate colors exist in colorTable, + // the result will be something like 0b00100000. + // 5. Use BitScanForward (bsf) to find the index of the most significant 1 bit. + const auto haystack = _mm256_loadu_si256(reinterpret_cast(colorTable.data())); // 1. + const auto needle = _mm256_set1_epi32(__builtin_bit_cast(int, defaultColor)); // 2. + const auto result = _mm256_cmpeq_epi32(haystack, needle); // 3. + const auto mask = _mm256_movemask_ps(_mm256_castsi256_ps(result)); // 4. + unsigned long index; + return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast(index) + 8) : defaultColor; // 5. +#elif _M_AMD64 + // If you look closely this SSE2 algorithm is the same as the AVX one. + // The two differences are that we need to: + // * do everything twice, because SSE is limited to 128 bits and not 256. + // * use _mm_packs_epi32 to merge two 128 bits vectors into one in step 3.5. + // _mm_packs_epi32 takes two SSE registers and truncates all 8 DWORDs into 8 WORDs, + // the latter of which fits into a single register (which is then used in the identical step 4). + // * since the result are now 8 WORDs, we need to use _mm_movemask_epi8 (there's no 16-bit variant), + // which unlike AVX's step 4 results in in something like 0b0000110000000000. + // --> the index returned by _BitScanForward must be divided by 2. + const auto haystack1 = _mm_loadu_si128(reinterpret_cast(colorTable.data() + 0)); + const auto haystack2 = _mm_loadu_si128(reinterpret_cast(colorTable.data() + 4)); + const auto needle = _mm_set1_epi32(__builtin_bit_cast(int, defaultColor)); + const auto result1 = _mm_cmpeq_epi32(haystack1, needle); + const auto result2 = _mm_cmpeq_epi32(haystack2, needle); + const auto result = _mm_packs_epi32(result1, result2); // 3.5 + const auto mask = _mm_movemask_epi8(result); + unsigned long index; + return _BitScanForward(&index, mask) ? til::at(colorTable, static_cast(index / 2) + 8) : defaultColor; +#else for (size_t i = 0; i < 8; i++) { if (til::at(colorTable, i) == defaultColor) @@ -163,6 +218,8 @@ COLORREF TextColor::GetColor(gsl::span colorTable, return til::at(colorTable, i + 8); } } +#endif +#pragma warning(pop) } return defaultColor; @@ -199,7 +256,7 @@ BYTE TextColor::GetLegacyIndex(const BYTE defaultIndex) const noexcept } else if (IsIndex256()) { - return Index256ToIndex16.at(GetIndex()); + return til::at(Index256ToIndex16, GetIndex()); } else { @@ -208,7 +265,7 @@ BYTE TextColor::GetLegacyIndex(const BYTE defaultIndex) const noexcept const BYTE compressedRgb = (_red & 0b11100000) + ((_green >> 3) & 0b00011100) + ((_blue >> 6) & 0b00000011); - return CompressedRgbToIndex16.at(compressedRgb); + return til::at(CompressedRgbToIndex16, compressedRgb); } } diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h index e8dcfcd82..ad984702f 100644 --- a/src/buffer/out/TextColor.h +++ b/src/buffer/out/TextColor.h @@ -86,10 +86,7 @@ public: void SetIndex(const BYTE index, const bool isIndex256) noexcept; void SetDefault() noexcept; - COLORREF GetColor(gsl::span colorTable, - const COLORREF defaultColor, - const bool brighten = false) const noexcept; - + COLORREF GetColor(const std::array& colorTable, const COLORREF defaultColor, bool brighten = false) const noexcept; BYTE GetLegacyIndex(const BYTE defaultIndex) const noexcept; constexpr BYTE GetIndex() const noexcept @@ -157,5 +154,3 @@ namespace WEX } } #endif - -static_assert(sizeof(TextColor) <= 4 * sizeof(BYTE), "We should only need 4B for an entire TextColor. Any more than that is just waste"); diff --git a/src/buffer/out/cursor.cpp b/src/buffer/out/cursor.cpp index 46f0208f8..1d050a215 100644 --- a/src/buffer/out/cursor.cpp +++ b/src/buffer/out/cursor.cpp @@ -315,6 +315,11 @@ void Cursor::StartDeferDrawing() noexcept _fDeferCursorRedraw = true; } +bool Cursor::IsDeferDrawing() noexcept +{ + return _fDeferCursorRedraw; +} + void Cursor::EndDeferDrawing() noexcept { if (_fHaveDeferredCursorRedraw) diff --git a/src/buffer/out/cursor.h b/src/buffer/out/cursor.h index 7ac93359a..dbd05f535 100644 --- a/src/buffer/out/cursor.h +++ b/src/buffer/out/cursor.h @@ -55,6 +55,7 @@ public: const COLORREF GetColor() const noexcept; void StartDeferDrawing() noexcept; + bool IsDeferDrawing() noexcept; void EndDeferDrawing() noexcept; void SetHasMoved(const bool fHasMoved) noexcept; diff --git a/src/buffer/out/lib/bufferout.vcxproj b/src/buffer/out/lib/bufferout.vcxproj index 2a5ea58de..cfe899c73 100644 --- a/src/buffer/out/lib/bufferout.vcxproj +++ b/src/buffer/out/lib/bufferout.vcxproj @@ -44,7 +44,7 @@ - + diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 76d127086..58de15c20 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -418,7 +418,6 @@ bool TextBuffer::InsertCharacter(const std::wstring_view chars, // Store character and double byte data CharRow& charRow = Row.GetCharRow(); - short const cBufferWidth = GetSize().Width(); try { @@ -1650,13 +1649,14 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF, if (!cell.DbcsAttr().IsTrailing()) { - selectionText.append(cell.Chars()); + const auto chars = cell.Chars(); + selectionText.append(chars); if (copyTextColor) { const auto cellData = cell.TextAttr(); const auto [CellFgAttr, CellBkAttr] = GetAttributeColors(cellData); - for (const wchar_t wch : cell.Chars()) + for (size_t j = 0; j < chars.size(); ++j) { selectionFgAttr.push_back(CellFgAttr); selectionBkAttr.push_back(CellBkAttr); diff --git a/src/buffer/out/textBufferCellIterator.cpp b/src/buffer/out/textBufferCellIterator.cpp index 64ed7acef..64caf0c98 100644 --- a/src/buffer/out/textBufferCellIterator.cpp +++ b/src/buffer/out/textBufferCellIterator.cpp @@ -94,20 +94,93 @@ bool TextBufferCellIterator::operator!=(const TextBufferCellIterator& it) const // - Reference to self after movement. TextBufferCellIterator& TextBufferCellIterator::operator+=(const ptrdiff_t& movement) { + // Note that this method is called intensively when the terminal is under heavy load. + // We need to aggressively optimize it, comparing to the -= operator. ptrdiff_t move = movement; - auto newPos = _pos; - while (move > 0 && !_exceeded) + if (move < 0) { - _exceeded = !_bounds.IncrementInBounds(newPos); + // Early branching to leave the rare case to -= operator. + // This helps reducing the instruction count within this method, which is good for instruction cache. + return *this -= -move; + } + + // The remaining code in this function is functionally equivalent to: + // auto newPos = _pos; + // while (move > 0 && !_exceeded) + // { + // _exceeded = !_bounds.IncrementInBounds(newPos); + // move--; + // } + // _SetPos(newPos); + // + // _SetPos() necessitates calling _GenerateView() and thus the construction + // of a new OutputCellView(). This has a high performance impact (ICache spill?). + // The code below inlines _bounds.IncrementInBounds as well as SetPos. + // In the hot path (_pos.Y doesn't change) we modify the _view directly. + + // Hoist these integers which will be used frequently later. + const auto boundsRightInclusive = _bounds.RightInclusive(); + const auto boundsLeft = _bounds.Left(); + const auto boundsBottomInclusive = _bounds.BottomInclusive(); + const auto boundsTop = _bounds.Top(); + const auto oldX = _pos.X; + const auto oldY = _pos.Y; + + // Under MSVC writing the individual members of a COORD generates worse assembly + // compared to having them be local variables. This causes a performance impact. + auto newX = oldX; + auto newY = oldY; + + while (move > 0) + { + if (newX == boundsRightInclusive) + { + newX = boundsLeft; + newY++; + if (newY > boundsBottomInclusive) + { + newY = boundsTop; + _exceeded = true; + break; + } + } + else + { + newX++; + _exceeded = false; + } move--; } - while (move < 0 && !_exceeded) + + if (_exceeded) { - _exceeded = !_bounds.DecrementInBounds(newPos); - move++; + // Early return because nothing needs to be done here. + return *this; } - _SetPos(newPos); - return (*this); + + if (newY == oldY) + { + // hot path + const auto diff = gsl::narrow_cast(newX) - gsl::narrow_cast(oldX); + _attrIter += diff; + _view.UpdateTextAttribute(*_attrIter); + + const CharRow& charRow = _pRow->GetCharRow(); + _view.UpdateText(charRow.GlyphAt(newX)); + _view.UpdateDbcsAttribute(charRow.DbcsAttrAt(newX)); + _pos.X = newX; + } + else + { + // cold path (_GenerateView is slow) + _pRow = s_GetRow(_buffer, { newX, newY }); + _attrIter = _pRow->GetAttrRow().cbegin() + newX; + _pos.X = newX; + _pos.Y = newY; + _GenerateView(); + } + + return *this; } // Routine Description: @@ -118,7 +191,22 @@ TextBufferCellIterator& TextBufferCellIterator::operator+=(const ptrdiff_t& move // - Reference to self after movement. TextBufferCellIterator& TextBufferCellIterator::operator-=(const ptrdiff_t& movement) { - return this->operator+=(-movement); + ptrdiff_t move = movement; + if (move < 0) + { + return (*this) += (-move); + } + + auto newPos = _pos; + while (move > 0 && !_exceeded) + { + _exceeded = !_bounds.DecrementInBounds(newPos); + move--; + } + _SetPos(newPos); + + _GenerateView(); + return (*this); } // Routine Description: diff --git a/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp b/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp index 44acc3551..65aba47f7 100644 --- a/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp +++ b/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp @@ -22,12 +22,11 @@ class TextAttributeTests TEST_METHOD(TestTextAttributeColorGetters); TEST_METHOD(TestReverseDefaultColors); TEST_METHOD(TestRoundtripDefaultColors); + TEST_METHOD(TestBoldAsBright); - static const int COLOR_TABLE_SIZE = 16; - COLORREF _colorTable[COLOR_TABLE_SIZE]; + std::array _colorTable; COLORREF _defaultFg = RGB(1, 2, 3); COLORREF _defaultBg = RGB(4, 5, 6); - gsl::span _GetTableView(); }; bool TextAttributeTests::ClassSetup() @@ -51,11 +50,6 @@ bool TextAttributeTests::ClassSetup() return true; } -gsl::span TextAttributeTests::_GetTableView() -{ - return gsl::span(&_colorTable[0], COLOR_TABLE_SIZE); -} - void TextAttributeTests::TestRoundtripLegacy() { WORD expectedLegacy = FOREGROUND_BLUE | BACKGROUND_RED; @@ -133,23 +127,22 @@ void TextAttributeTests::TestTextAttributeColorGetters() const COLORREF faintRed = RGB(127, 0, 0); const COLORREF green = RGB(0, 255, 0); TextAttribute attr(red, green); - auto view = _GetTableView(); // verify that calculated foreground/background are the same as the direct // values when reverse video is not set VERIFY_IS_FALSE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(red, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(red, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, calculated foreground/background values should be // switched while getters stay the same attr.SetReverseVideo(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(green, red), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(green, red), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // reset the reverse video attr.SetReverseVideo(false); @@ -158,17 +151,17 @@ void TextAttributeTests::TestTextAttributeColorGetters() // while the background and getters stay the same attr.SetFaint(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(faintRed, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(faintRed, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, calculated foreground/background values should be // switched, and the background fainter, while getters stay the same attr.SetReverseVideo(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(green, faintRed), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(green, faintRed), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // reset the reverse video and faint attributes attr.SetReverseVideo(false); @@ -178,17 +171,17 @@ void TextAttributeTests::TestTextAttributeColorGetters() // background, while getters stay the same attr.SetInvisible(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(green, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(green, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, the calculated background value should match // the foreground, while getters stay the same attr.SetReverseVideo(true); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(red, red), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(red, red), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); } void TextAttributeTests::TestReverseDefaultColors() @@ -196,40 +189,39 @@ void TextAttributeTests::TestReverseDefaultColors() const COLORREF red = RGB(255, 0, 0); const COLORREF green = RGB(0, 255, 0); TextAttribute attr{}; - auto view = _GetTableView(); // verify that calculated foreground/background are the same as the direct // values when reverse video is not set VERIFY_IS_FALSE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); // with reverse video set, calculated foreground/background values should be // switched while getters stay the same attr.SetReverseVideo(true); VERIFY_IS_TRUE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, _defaultFg), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, _defaultFg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); attr.SetForeground(red); VERIFY_IS_TRUE(attr.IsReverseVideo()); - VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, red), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultBg, red), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); attr.Invert(); VERIFY_IS_FALSE(attr.IsReverseVideo()); attr.SetDefaultForeground(); attr.SetBackground(green); - VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(view, _defaultFg)); - VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(view, _defaultBg)); - VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, green), attr.CalculateRgbColors(view, _defaultFg, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, green), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg)); } void TextAttributeTests::TestRoundtripDefaultColors() @@ -272,3 +264,56 @@ void TextAttributeTests::TestRoundtripDefaultColors() // Reset the legacy default colors to white on black. TextAttribute::SetLegacyDefaultAttributes(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); } + +void TextAttributeTests::TestBoldAsBright() +{ + const COLORREF darkBlack = til::at(_colorTable, 0); + const COLORREF brightBlack = til::at(_colorTable, 8); + const COLORREF darkGreen = til::at(_colorTable, 2); + + TextAttribute attr{}; + + // verify that calculated foreground/background are the same as the direct + // values when not bold + VERIFY_IS_FALSE(attr.IsBold()); + + VERIFY_ARE_EQUAL(_defaultFg, attr.GetForeground().GetColor(_colorTable, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.GetBackground().GetColor(_colorTable, _defaultBg)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false)); + + // with bold set, calculated foreground/background values shouldn't change for the default colors. + attr.SetBold(true); + VERIFY_IS_TRUE(attr.IsBold()); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true)); + VERIFY_ARE_EQUAL(std::make_pair(_defaultFg, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false)); + + attr.SetIndexedForeground(0); + VERIFY_IS_TRUE(attr.IsBold()); + + Log::Comment(L"Foreground should be bright black when bold is bright is enabled"); + VERIFY_ARE_EQUAL(std::make_pair(brightBlack, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true)); + + Log::Comment(L"Foreground should be dark black when bold is bright is disabled"); + VERIFY_ARE_EQUAL(std::make_pair(darkBlack, _defaultBg), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false)); + + attr.SetIndexedBackground(2); + VERIFY_IS_TRUE(attr.IsBold()); + + Log::Comment(L"background should be unaffected by 'bold is bright'"); + VERIFY_ARE_EQUAL(std::make_pair(brightBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true)); + VERIFY_ARE_EQUAL(std::make_pair(darkBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false)); + + attr.SetBold(false); + VERIFY_IS_FALSE(attr.IsBold()); + Log::Comment(L"when not bold, 'bold is bright' changes nothing"); + VERIFY_ARE_EQUAL(std::make_pair(darkBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true)); + VERIFY_ARE_EQUAL(std::make_pair(darkBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false)); + + Log::Comment(L"When set to a bright color, and bold, 'bold is bright' changes nothing"); + attr.SetBold(true); + attr.SetIndexedForeground(8); + VERIFY_IS_TRUE(attr.IsBold()); + VERIFY_ARE_EQUAL(std::make_pair(brightBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, true)); + VERIFY_ARE_EQUAL(std::make_pair(brightBlack, darkGreen), attr.CalculateRgbColors(_colorTable, _defaultFg, _defaultBg, false, false, false)); +} diff --git a/src/buffer/out/ut_textbuffer/TextColorTests.cpp b/src/buffer/out/ut_textbuffer/TextColorTests.cpp index 8d068658b..d419bfc33 100644 --- a/src/buffer/out/ut_textbuffer/TextColorTests.cpp +++ b/src/buffer/out/ut_textbuffer/TextColorTests.cpp @@ -23,11 +23,9 @@ class TextColorTests TEST_METHOD(TestRgbColor); TEST_METHOD(TestChangeColor); - static const int COLOR_TABLE_SIZE = 16; - COLORREF _colorTable[COLOR_TABLE_SIZE]; + std::array _colorTable; COLORREF _defaultFg = RGB(1, 2, 3); COLORREF _defaultBg = RGB(4, 5, 6); - gsl::span _GetTableView(); }; bool TextColorTests::ClassSetup() @@ -51,11 +49,6 @@ bool TextColorTests::ClassSetup() return true; } -gsl::span TextColorTests::_GetTableView() -{ - return gsl::span(&_colorTable[0], COLOR_TABLE_SIZE); -} - void TextColorTests::TestDefaultColor() { TextColor defaultColor; @@ -64,18 +57,16 @@ void TextColorTests::TestDefaultColor() VERIFY_IS_FALSE(defaultColor.IsLegacy()); VERIFY_IS_FALSE(defaultColor.IsRgb()); - auto view = _GetTableView(); - - auto color = defaultColor.GetColor(view, _defaultFg, false); + auto color = defaultColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_defaultFg, color); - color = defaultColor.GetColor(view, _defaultFg, true); + color = defaultColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_defaultFg, color); - color = defaultColor.GetColor(view, _defaultBg, false); + color = defaultColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_defaultBg, color); - color = defaultColor.GetColor(view, _defaultBg, true); + color = defaultColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_defaultBg, color); } @@ -87,18 +78,16 @@ void TextColorTests::TestDarkIndexColor() VERIFY_IS_TRUE(indexColor.IsLegacy()); VERIFY_IS_FALSE(indexColor.IsRgb()); - auto view = _GetTableView(); - - auto color = indexColor.GetColor(view, _defaultFg, false); + auto color = indexColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = indexColor.GetColor(view, _defaultFg, true); + color = indexColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultBg, false); + color = indexColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = indexColor.GetColor(view, _defaultBg, true); + color = indexColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); } @@ -110,18 +99,16 @@ void TextColorTests::TestBrightIndexColor() VERIFY_IS_TRUE(indexColor.IsLegacy()); VERIFY_IS_FALSE(indexColor.IsRgb()); - auto view = _GetTableView(); - - auto color = indexColor.GetColor(view, _defaultFg, false); + auto color = indexColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultFg, true); + color = indexColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultBg, false); + color = indexColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = indexColor.GetColor(view, _defaultBg, true); + color = indexColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); } @@ -134,18 +121,16 @@ void TextColorTests::TestRgbColor() VERIFY_IS_FALSE(rgbColor.IsLegacy()); VERIFY_IS_TRUE(rgbColor.IsRgb()); - auto view = _GetTableView(); - - auto color = rgbColor.GetColor(view, _defaultFg, false); + auto color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(myColor, color); } @@ -158,57 +143,55 @@ void TextColorTests::TestChangeColor() VERIFY_IS_FALSE(rgbColor.IsLegacy()); VERIFY_IS_TRUE(rgbColor.IsRgb()); - auto view = _GetTableView(); - - auto color = rgbColor.GetColor(view, _defaultFg, false); + auto color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(myColor, color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(myColor, color); rgbColor.SetDefault(); - color = rgbColor.GetColor(view, _defaultFg, false); + color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_defaultFg, color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_defaultFg, color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_defaultBg, color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_defaultBg, color); rgbColor.SetIndex(7, false); - color = rgbColor.GetColor(view, _defaultFg, false); + color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[7], color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); rgbColor.SetIndex(15, false); - color = rgbColor.GetColor(view, _defaultFg, false); + color = rgbColor.GetColor(_colorTable, _defaultFg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultFg, true); + color = rgbColor.GetColor(_colorTable, _defaultFg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultBg, false); + color = rgbColor.GetColor(_colorTable, _defaultBg, false); VERIFY_ARE_EQUAL(_colorTable[15], color); - color = rgbColor.GetColor(view, _defaultBg, true); + color = rgbColor.GetColor(_colorTable, _defaultBg, true); VERIFY_ARE_EQUAL(_colorTable[15], color); } diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index b18c6d57a..cf19029fc 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -146,12 +146,12 @@ - + 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/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp index ce5f06014..bdc4d5692 100644 --- a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp @@ -397,6 +397,10 @@ namespace SettingsModelLocalTests "name":"action6", "command": { "action": "newWindow", "startingDirectory":"C:\\foo", "commandline": "bar.exe" } }, + { + "name":"action7_startingDirectoryWithTrailingSlash", + "command": { "action": "newWindow", "startingDirectory":"C:\\", "commandline": "bar.exe" } + }, ])" }; const auto commands0Json = VerifyParseSucceeded(commands0String); @@ -405,7 +409,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, commands.Size()); auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(7u, commands.Size()); + VERIFY_ARE_EQUAL(8u, commands.Size()); { auto command = commands.Lookup(L"action0"); @@ -503,5 +507,20 @@ namespace SettingsModelLocalTests L"cmdline: \"%s\"", cmdline.c_str())); VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\foo\" -- \"bar.exe\"", terminalArgs.ToCommandline()); } + + { + auto command = commands.Lookup(L"action7_startingDirectoryWithTrailingSlash"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.ActionAndArgs()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.ActionAndArgs().Action()); + const auto& realArgs = command.ActionAndArgs().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + Log::Comment(NoThrowString().Format( + L"cmdline: \"%s\"", cmdline.c_str())); + VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\\\\" -- \"bar.exe\"", terminalArgs.ToCommandline()); + } } } diff --git a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp index 62df2a1d2..bff6e7e17 100644 --- a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp @@ -1971,9 +1971,9 @@ namespace SettingsModelLocalTests auto settings = implementation::CascadiaSettings::FromJson(settingsObject); VERIFY_ARE_EQUAL(3u, settings->_globals->_actionMap->_KeyMap.size()); - VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('a') })); - VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('b') })); - VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('A'), 0 })); + VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('B'), 0 })); + VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); for (const auto& warning : settings->_globals->_keybindingsWarnings) { @@ -2124,7 +2124,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(1u, nameMap.Size()); { - KeyChord kc{ true, false, false, static_cast('A') }; + KeyChord kc{ true, false, false, false, static_cast('A'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2141,7 +2141,7 @@ namespace SettingsModelLocalTests Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set."); { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2155,7 +2155,7 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2169,7 +2169,7 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { - KeyChord kc{ true, false, false, static_cast('E') }; + KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2183,7 +2183,7 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -2841,7 +2841,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); VERIFY_ARE_EQUAL(1u, nameMap.Size()); - const KeyChord expectedKeyChord{ true, false, true, static_cast('W') }; + const KeyChord expectedKeyChord{ true, false, true, false, static_cast('W'), 0 }; { // Verify NameMap returns correct value const auto& cmd{ nameMap.TryLookup(L"foo") }; diff --git a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index 47a59d0fd..946eda0f1 100644 --- a/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -36,10 +36,15 @@ namespace SettingsModelLocalTests TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") END_TEST_CLASS() + TEST_METHOD(KeyChords); TEST_METHOD(ManyKeysSameAction); TEST_METHOD(LayerKeybindings); TEST_METHOD(UnbindKeybindings); + TEST_METHOD(LayerScancodeKeybindings); + + TEST_METHOD(TestExplicitUnbind); + TEST_METHOD(TestArbitraryArgs); TEST_METHOD(TestSplitPaneArgs); @@ -53,6 +58,7 @@ namespace SettingsModelLocalTests TEST_METHOD(TestMoveTabArgs); TEST_METHOD(TestGetKeyBindingForAction); + TEST_METHOD(KeybindingsWithoutVkey); TEST_CLASS_SETUP(ClassSetup) { @@ -61,6 +67,69 @@ namespace SettingsModelLocalTests } }; + void KeyBindingsTests::KeyChords() + { + struct testCase + { + VirtualKeyModifiers modifiers; + int32_t vkey; + int32_t scanCode; + std::wstring_view expected; + }; + + static constexpr std::array testCases{ + testCase{ + VirtualKeyModifiers::None, + 'A', + 0, + L"a", + }, + testCase{ + VirtualKeyModifiers::Control, + 'A', + 0, + L"ctrl+a", + }, + testCase{ + VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, + VK_OEM_PLUS, + 0, + L"ctrl+shift+plus", + }, + testCase{ + VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows, + 255, + 0, + L"win+ctrl+alt+shift+vk(255)", + }, + testCase{ + VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows, + 0, + 123, + L"win+ctrl+alt+shift+sc(123)", + }, + }; + + for (const auto& tc : testCases) + { + Log::Comment(NoThrowString().Format(L"Testing case:\"%s\"", tc.expected.data())); + + const auto actualString = KeyChordSerialization::ToString({ tc.modifiers, tc.vkey, tc.scanCode }); + VERIFY_ARE_EQUAL(tc.expected, actualString); + + auto expectedVkey = tc.vkey; + if (!expectedVkey) + { + expectedVkey = MapVirtualKeyW(tc.scanCode, MAPVK_VSC_TO_VK_EX); + } + + const auto actualKeyChord = KeyChordSerialization::FromString(actualString); + VERIFY_ARE_EQUAL(tc.modifiers, actualKeyChord.Modifiers()); + VERIFY_ARE_EQUAL(expectedVkey, actualKeyChord.Vkey()); + VERIFY_ARE_EQUAL(tc.scanCode, actualKeyChord.ScanCode()); + } + } + void KeyBindingsTests::ManyKeysSameAction() { const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; @@ -75,7 +144,6 @@ namespace SettingsModelLocalTests const auto bindings2Json = VerifyParseSucceeded(bindings2String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); @@ -99,7 +167,6 @@ namespace SettingsModelLocalTests const auto bindings2Json = VerifyParseSucceeded(bindings2String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); @@ -129,7 +196,6 @@ namespace SettingsModelLocalTests const auto bindings5Json = VerifyParseSucceeded(bindings5String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); @@ -142,7 +208,7 @@ namespace SettingsModelLocalTests L"Try unbinding a key using `\"unbound\"` to unbind the key")); actionMap->LayerJson(bindings2Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key using `null` to unbind the key")); @@ -152,7 +218,7 @@ namespace SettingsModelLocalTests // Then try layering in the bad setting actionMap->LayerJson(bindings3Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key using an unrecognized command to unbind the key")); @@ -162,7 +228,7 @@ namespace SettingsModelLocalTests // Then try layering in the bad setting actionMap->LayerJson(bindings4Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key using a straight up invalid value to unbind the key")); @@ -172,13 +238,38 @@ namespace SettingsModelLocalTests // Then try layering in the bad setting actionMap->LayerJson(bindings5Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); Log::Comment(NoThrowString().Format( L"Try unbinding a key that wasn't bound at all")); actionMap->LayerJson(bindings2Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); - VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('c') })); + VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); + } + + void KeyBindingsTests::TestExplicitUnbind() + { + const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; + const std::string bindings1String{ R"([ { "command": "unbound", "keys": ["ctrl+c"] } ])" }; + const std::string bindings2String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + const auto bindings1Json = VerifyParseSucceeded(bindings1String); + const auto bindings2Json = VerifyParseSucceeded(bindings2String); + + const KeyChord keyChord{ VirtualKeyModifiers::Control, static_cast('C'), 0 }; + + auto actionMap = winrt::make_self(); + VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); + + actionMap->LayerJson(bindings0Json); + VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); + + actionMap->LayerJson(bindings1Json); + VERIFY_IS_TRUE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); + + actionMap->LayerJson(bindings2Json); + VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord)); } void KeyBindingsTests::TestArbitraryArgs() @@ -203,7 +294,6 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(10u, actionMap->_KeyMap.size()); @@ -211,10 +301,9 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` without args parses as Copy(SingleLine=false)")); - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -222,10 +311,9 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); - KeyChord kc{ true, false, true, static_cast('C') }; + KeyChord kc{ true, false, true, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -233,10 +321,9 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); - KeyChord kc{ false, true, true, static_cast('C') }; + KeyChord kc{ false, true, true, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_TRUE(realArgs.SingleLine()); } @@ -244,11 +331,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `newTab` without args parses as NewTab(Index=null)")); - KeyChord kc{ true, false, false, static_cast('T') }; + KeyChord kc{ true, false, false, false, static_cast('T'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_NULL(realArgs.TerminalArgs().ProfileIndex()); @@ -256,11 +342,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `newTab` parses args correctly")); - KeyChord kc{ true, false, true, static_cast('T') }; + KeyChord kc{ true, false, true, false, static_cast('T'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); @@ -270,11 +355,10 @@ namespace SettingsModelLocalTests Log::Comment(NoThrowString().Format( L"Verify that `newTab` with an index greater than the legacy " L"args afforded parses correctly")); - KeyChord kc{ true, false, true, static_cast('Y') }; + KeyChord kc{ true, false, true, false, static_cast('Y'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); @@ -284,11 +368,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` ignores args it doesn't understand")); - KeyChord kc{ true, false, true, static_cast('B') }; + KeyChord kc{ true, false, true, false, static_cast('B'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -296,11 +379,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `copy` null as it's `args` parses as the default option")); - KeyChord kc{ true, false, true, static_cast('B') }; + KeyChord kc{ true, false, true, false, static_cast('B'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -308,11 +390,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `adjustFontSize` with a positive delta parses args correctly")); - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(1, realArgs.Delta()); } @@ -320,11 +401,10 @@ namespace SettingsModelLocalTests { Log::Comment(NoThrowString().Format( L"Verify that `adjustFontSize` with a negative delta parses args correctly")); - KeyChord kc{ true, false, false, static_cast('G') }; + KeyChord kc{ true, false, false, false, static_cast('G'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(-1, realArgs.Delta()); } @@ -342,44 +422,39 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(4u, actionMap->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); } { - KeyChord kc{ true, false, false, static_cast('E') }; + KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); } { - KeyChord kc{ true, false, false, static_cast('G') }; + KeyChord kc{ true, false, false, false, static_cast('G'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } { - KeyChord kc{ true, false, false, static_cast('H') }; + KeyChord kc{ true, false, false, false, static_cast('H'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } @@ -396,37 +471,33 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.TabColor()); } { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TabColor()); // Remember that COLORREFs are actually BBGGRR order, while the string is in #RRGGBB order VERIFY_ARE_EQUAL(til::color(0x563412), til::color(realArgs.TabColor().Value())); } { - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.TabColor()); } @@ -441,16 +512,14 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } @@ -470,63 +539,56 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(6u, actionMap->_KeyMap.size()); { - KeyChord kc{ false, false, false, static_cast(VK_UP) }; + KeyChord kc{ false, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ false, false, false, static_cast(VK_DOWN) }; + KeyChord kc{ false, false, false, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ true, false, false, static_cast(VK_UP) }; + KeyChord kc{ true, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ true, false, false, static_cast(VK_DOWN) }; + KeyChord kc{ true, false, false, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NULL(realArgs.RowsToScroll()); } { - KeyChord kc{ true, false, true, static_cast(VK_UP) }; + KeyChord kc{ true, false, true, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.RowsToScroll()); VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value()); } { - KeyChord kc{ true, false, true, static_cast(VK_DOWN) }; + KeyChord kc{ true, false, true, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.RowsToScroll()); VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value()); @@ -535,7 +597,6 @@ namespace SettingsModelLocalTests const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "scrollDown", "rowsToScroll": -1 } }])" }; const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); auto invalidActionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(invalidActionMap); VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size()); VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception); } @@ -551,26 +612,23 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size()); { - KeyChord kc{ false, false, false, static_cast(VK_UP) }; + KeyChord kc{ false, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::MoveTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.Direction(), MoveTabDirection::Forward); } { - KeyChord kc{ false, false, false, static_cast(VK_DOWN) }; + KeyChord kc{ false, false, false, false, static_cast(VK_DOWN), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::MoveTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.Direction(), MoveTabDirection::Backward); } @@ -584,7 +642,6 @@ namespace SettingsModelLocalTests const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "moveTab", "direction": "bad" } }])" }; const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); auto invalidActionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(invalidActionMap); VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size()); VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception); } @@ -601,35 +658,31 @@ namespace SettingsModelLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size()); { - KeyChord kc{ false, false, false, static_cast(VK_UP) }; + KeyChord kc{ false, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::Action); } { - KeyChord kc{ true, false, false, static_cast(VK_UP) }; + KeyChord kc{ true, false, false, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::Action); } { - KeyChord kc{ true, false, true, static_cast(VK_UP) }; + KeyChord kc{ true, false, true, false, static_cast(VK_UP), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); + const auto& realArgs = actionAndArgs.Args().as(); // Verify the args have the expected value VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::CommandLine); } @@ -637,7 +690,6 @@ namespace SettingsModelLocalTests const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "commandPalette", "launchMode": "bad" } }])" }; const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); auto invalidActionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(invalidActionMap); VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size()); VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception); } @@ -669,7 +721,6 @@ namespace SettingsModelLocalTests }; auto actionMap = winrt::make_self(); - VERIFY_IS_NOT_NULL(actionMap); VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); { @@ -677,7 +728,7 @@ namespace SettingsModelLocalTests actionMap->LayerJson(bindings0Json); VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::CloseWindow) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('A') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('A'), 0 }, kbd); } { Log::Comment(L"command with args"); @@ -688,7 +739,7 @@ namespace SettingsModelLocalTests args->SingleLine(true); const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::CopyText, *args) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('B') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('B'), 0 }, kbd); } { Log::Comment(L"command with new terminal args"); @@ -700,7 +751,7 @@ namespace SettingsModelLocalTests auto args{ winrt::make_self(*newTerminalArgs) }; const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::NewTab, *args) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('C') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast('C'), 0 }, kbd); } { Log::Comment(L"command with hidden args"); @@ -708,7 +759,45 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(4u, actionMap->_KeyMap.size()); const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::ToggleCommandPalette) }; - VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast('P') }, kbd); + VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast('P'), 0 }, kbd); } } + + void KeyBindingsTests::LayerScancodeKeybindings() + { + Log::Comment(L"Layering a keybinding with a character literal on top of" + L" an equivalent sc() key should replace it."); + + // Wrap the first one in `R"!(...)!"` because it has `()` internally. + const std::string bindings0String{ R"!([ { "command": "quakeMode", "keys":"win+sc(41)" } ])!" }; + const std::string bindings1String{ R"([ { "keys": "win+`", "command": { "action": "globalSummon", "monitor": "any" } } ])" }; + const std::string bindings2String{ R"([ { "keys": "ctrl+shift+`", "command": { "action": "quakeMode" } } ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + const auto bindings1Json = VerifyParseSucceeded(bindings1String); + const auto bindings2Json = VerifyParseSucceeded(bindings2String); + + auto actionMap = winrt::make_self(); + VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size()); + + actionMap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size()); + + actionMap->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size(), L"Layering the second action should replace the first one."); + + actionMap->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size()); + } + + void KeyBindingsTests::KeybindingsWithoutVkey() + { + const auto json = VerifyParseSucceeded(R"!([{"command": "quakeMode", "keys":"shift+sc(255)"}])!"); + + const auto actionMap = winrt::make_self(); + actionMap->LayerJson(json); + + const auto action = actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Shift, 0, 255 }); + VERIFY_IS_NOT_NULL(action); + } } diff --git a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj index ac1af0396..93b891077 100644 --- a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj @@ -98,10 +98,10 @@ x86 $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.2.5.0-prerelease.201202003\runtimes\win10-$(Native-Platform)\native\" + <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.2.6.2-prerelease.210818003\runtimes\win10-$(Native-Platform)\native\" - + diff --git a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp index 459fadc18..e274767d3 100644 --- a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp @@ -37,7 +37,7 @@ namespace SettingsModelLocalTests TEST_METHOD(TestTerminalArgsForBinding); - TEST_METHOD(MakeSettingsForProfileThatDoesntExist); + TEST_METHOD(MakeSettingsForProfile); TEST_METHOD(MakeSettingsForDefaultProfileThatDoesntExist); TEST_METHOD(TestLayerProfileOnColorScheme); @@ -113,7 +113,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(12u, actionMapImpl->_KeyMap.size()); { - KeyChord kc{ true, false, false, static_cast('A') }; + KeyChord kc{ true, false, false, false, static_cast('A'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -126,15 +126,15 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(guid0, profile.Guid()); VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('B') }; + KeyChord kc{ true, false, false, false, static_cast('B'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -148,15 +148,15 @@ namespace SettingsModelLocalTests VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}", realArgs.TerminalArgs().Profile()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(guid1, profile.Guid()); VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('C') }; + KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -170,15 +170,15 @@ namespace SettingsModelLocalTests VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(guid1, profile.Guid()); VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('D') }; + KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -192,15 +192,15 @@ namespace SettingsModelLocalTests VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(profile2Guid, profile.Guid()); VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('E') }; + KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -214,15 +214,15 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(guid0, profile.Guid()); VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('F') }; + KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -237,15 +237,15 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(guid1, profile.Guid()); VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('G') }; + KeyChord kc{ true, false, false, false, static_cast('G'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -257,15 +257,15 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(guid0, profile.Guid()); VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('H') }; + KeyChord kc{ true, false, false, false, static_cast('H'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -278,16 +278,16 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(guid0, profile.Guid()); VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('I') }; + KeyChord kc{ true, false, false, false, static_cast('I'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -301,16 +301,16 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(profile2Guid, profile.Guid()); VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('J') }; + KeyChord kc{ true, false, false, false, static_cast('J'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -323,16 +323,16 @@ namespace SettingsModelLocalTests VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(guid0, profile.Guid()); VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('K') }; + KeyChord kc{ true, false, false, false, static_cast('K'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -346,16 +346,16 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(profile2Guid, profile.Guid()); VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } { - KeyChord kc{ true, false, false, static_cast('L') }; + KeyChord kc{ true, false, false, false, static_cast('L'), 0 }; auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); @@ -371,10 +371,10 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); - const auto guid{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; + const auto profile{ settings.GetProfileForArgs(realArgs.TerminalArgs()) }; const auto settingsStruct{ TerminalSettings::CreateWithNewTerminalArgs(settings, realArgs.TerminalArgs(), nullptr) }; const auto termSettings = settingsStruct.DefaultSettings(); - VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(guid1, profile.Guid()); VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); @@ -382,9 +382,9 @@ namespace SettingsModelLocalTests } } - void TerminalSettingsTests::MakeSettingsForProfileThatDoesntExist() + void TerminalSettingsTests::MakeSettingsForProfile() { - // Test that making settings throws when the GUID doesn't exist + // Test that making settings generally works. const std::string settingsString{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", @@ -405,32 +405,32 @@ namespace SettingsModelLocalTests const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); const auto guid2 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); - const auto guid3 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + + const auto profile1 = settings.FindProfile(guid1); + const auto profile2 = settings.FindProfile(guid2); try { - auto terminalSettings{ TerminalSettings::CreateWithProfileByID(settings, guid1, nullptr) }; + auto terminalSettings{ TerminalSettings::CreateWithProfile(settings, profile1, nullptr) }; VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings); VERIFY_ARE_EQUAL(1, terminalSettings.DefaultSettings().HistorySize()); } catch (...) { - VERIFY_IS_TRUE(false, L"This call to CreateWithProfileByID should succeed"); + VERIFY_IS_TRUE(false, L"This call to CreateWithProfile should succeed"); } try { - auto terminalSettings{ TerminalSettings::CreateWithProfileByID(settings, guid2, nullptr) }; + auto terminalSettings{ TerminalSettings::CreateWithProfile(settings, profile2, nullptr) }; VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings); VERIFY_ARE_EQUAL(2, terminalSettings.DefaultSettings().HistorySize()); } catch (...) { - VERIFY_IS_TRUE(false, L"This call to CreateWithProfileByID should succeed"); + VERIFY_IS_TRUE(false, L"This call to CreateWithProfile should succeed"); } - VERIFY_THROWS(auto terminalSettings = TerminalSettings::CreateWithProfileByID(settings, guid3, nullptr), wil::ResultException, L"This call to constructor should fail"); - try { const auto termSettings{ TerminalSettings::CreateWithNewTerminalArgs(settings, nullptr, nullptr) }; diff --git a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp index f62002e6f..8384690c4 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp +++ b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp @@ -56,6 +56,7 @@ namespace TerminalAppLocalTests TEST_METHOD(ParseComboCommandlineIntoArgs); TEST_METHOD(ParseFocusTabArgs); TEST_METHOD(ParseMoveFocusArgs); + TEST_METHOD(ParseSwapPaneArgs); TEST_METHOD(ParseArgumentsWithParsingTerminators); TEST_METHOD(ParseFocusPaneArgs); @@ -1207,6 +1208,119 @@ namespace TerminalAppLocalTests } } + void CommandlineTest::ParseSwapPaneArgs() + { + const wchar_t* subcommand = L"swap-pane"; + + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand }; + Log::Comment(NoThrowString().Format( + L"Just the subcommand, without a direction, should fail.")); + + _buildCommandlinesExpectFailureHelper(appArgs, 1u, rawCommands); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"left" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"right" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"up" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Up, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"down" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(2u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Down, myArgs.Direction()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"badDirection" }; + Log::Comment(NoThrowString().Format( + L"move-pane with an invalid direction should fail.")); + _buildCommandlinesExpectFailureHelper(appArgs, 1u, rawCommands); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"left", L";", subcommand, L"right" }; + _buildCommandlinesHelper(appArgs, 2u, rawCommands); + + VERIFY_ARE_EQUAL(3u, appArgs._startupActions.size()); + + // The first action is going to always be a new-tab action + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, appArgs._startupActions.at(0).Action()); + + auto actionAndArgs = appArgs._startupActions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Left, myArgs.Direction()); + + actionAndArgs = appArgs._startupActions.at(2); + VERIFY_ARE_EQUAL(ShortcutAction::SwapPane, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_ARE_EQUAL(FocusDirection::Right, myArgs.Direction()); + } + } + void CommandlineTest::ParseFocusPaneArgs() { BEGIN_TEST_METHOD_PROPERTIES() diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index f56deecd4..07323750b 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -82,6 +82,8 @@ namespace TerminalAppLocalTests TEST_METHOD(MoveFocusFromZoomedPane); TEST_METHOD(CloseZoomedPane); + TEST_METHOD(SwapPanes); + TEST_METHOD(NextMRUTab); TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder); @@ -93,6 +95,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TestPreviewDismissScheme); TEST_METHOD(TestPreviewSchemeWhilePreviewing); + TEST_METHOD(TestClampSwitchToTab); + TEST_CLASS_SETUP(ClassSetup) { return true; @@ -426,7 +430,7 @@ namespace TerminalAppLocalTests Log::Comment(L"Duplicate the tab, and don't crash"); result = RunOnUIThread([&page]() { page->_DuplicateFocusedTab(); - VERIFY_ARE_EQUAL(2u, page->_tabs.Size(), L"We should gracefully do nothing here - the profile no longer exists."); + VERIFY_ARE_EQUAL(3u, page->_tabs.Size(), L"We should successfully duplicate a tab hosting a deleted profile."); }); VERIFY_SUCCEEDED(result); } @@ -526,9 +530,9 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); - VERIFY_ARE_EQUAL(2, + VERIFY_ARE_EQUAL(3, tab->GetLeafPaneCount(), - L"We should gracefully do nothing here - the profile no longer exists."); + L"We should successfully duplicate a pane hosting a deleted profile."); }); VERIFY_SUCCEEDED(result); @@ -817,6 +821,212 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(result); } + void TabTests::SwapPanes() + { + auto page = _commonSetup(); + + Log::Comment(L"Setup 4 panes."); + // Create the following layout + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + // | 3 | 4 | + // | | | + // ------------------- + uint32_t firstId = 0, secondId = 0, thirdId = 0, fourthId = 0; + TestOnUIThread([&]() { + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + firstId = tab->_activePane->Id().value(); + // We start with 1 tab, split vertically to get + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + page->_SplitPane(SplitState::Vertical, SplitType::Duplicate, 0.5f, nullptr); + secondId = tab->_activePane->Id().value(); + }); + Sleep(250); + TestOnUIThread([&]() { + // After this the `2` pane is focused, go back to `1` being focused + page->_MoveFocus(FocusDirection::Left); + }); + Sleep(250); + TestOnUIThread([&]() { + // Split again to make the 3rd tab + // ------------------- + // | 1 | | + // | | | + // ---------| 2 | + // | 3 | | + // | | | + // ------------------- + page->_SplitPane(SplitState::Horizontal, SplitType::Duplicate, 0.5f, nullptr); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + // Split again to make the 3rd tab + thirdId = tab->_activePane->Id().value(); + }); + Sleep(250); + TestOnUIThread([&]() { + // After this the `3` pane is focused, go back to `2` being focused + page->_MoveFocus(FocusDirection::Right); + }); + Sleep(250); + TestOnUIThread([&]() { + // Split to create the final pane + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + // | 3 | 4 | + // | | | + // ------------------- + page->_SplitPane(SplitState::Horizontal, SplitType::Duplicate, 0.5f, nullptr); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + fourthId = tab->_activePane->Id().value(); + }); + + Sleep(250); + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // just to be complete, make sure we actually have 4 different ids + VERIFY_ARE_NOT_EQUAL(firstId, fourthId); + VERIFY_ARE_NOT_EQUAL(secondId, fourthId); + VERIFY_ARE_NOT_EQUAL(thirdId, fourthId); + VERIFY_ARE_NOT_EQUAL(firstId, thirdId); + VERIFY_ARE_NOT_EQUAL(secondId, thirdId); + VERIFY_ARE_NOT_EQUAL(firstId, secondId); + }); + + // Gratuitous use of sleep to make sure that the UI has updated properly + // after each operation. + Sleep(250); + // Now try to move the pane through the tree + Log::Comment(L"Move pane to the left. This should swap panes 3 and 4"); + // ------------------- + // | 1 | 2 | + // | | | + // ------------------- + // | 4 | 3 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + SwapPaneArgs args{ FocusDirection::Left }; + ActionEventArgs eventArgs{ args }; + + page->_HandleSwapPane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_firstChild->_secondChild->Id().value()); + VERIFY_ARE_EQUAL(thirdId, tab->_rootPane->_secondChild->_secondChild->Id().value()); + }); + + Sleep(250); + + Log::Comment(L"Move pane to up. This should swap panes 1 and 4"); + // ------------------- + // | 4 | 2 | + // | | | + // ------------------- + // | 1 | 3 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + SwapPaneArgs args{ FocusDirection::Up }; + ActionEventArgs eventArgs{ args }; + + page->_HandleSwapPane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_firstChild->_firstChild->Id().value()); + VERIFY_ARE_EQUAL(firstId, tab->_rootPane->_firstChild->_secondChild->Id().value()); + }); + + Sleep(250); + + Log::Comment(L"Move pane to the right. This should swap panes 2 and 4"); + // ------------------- + // | 2 | 4 | + // | | | + // ------------------- + // | 1 | 3 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + SwapPaneArgs args{ FocusDirection::Right }; + ActionEventArgs eventArgs{ args }; + + page->_HandleSwapPane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_secondChild->_firstChild->Id().value()); + VERIFY_ARE_EQUAL(secondId, tab->_rootPane->_firstChild->_firstChild->Id().value()); + }); + + Sleep(250); + + Log::Comment(L"Move pane down. This should swap panes 3 and 4"); + // ------------------- + // | 2 | 3 | + // | | | + // ------------------- + // | 1 | 4 | + // | | | + // ------------------- + TestOnUIThread([&]() { + // Set up action + SwapPaneArgs args{ FocusDirection::Down }; + ActionEventArgs eventArgs{ args }; + + page->_HandleSwapPane(nullptr, eventArgs); + }); + + Sleep(250); + + TestOnUIThread([&]() { + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(4, tab->GetLeafPaneCount()); + // Our currently focused pane should be `4` + VERIFY_ARE_EQUAL(fourthId, tab->_activePane->Id().value()); + + // Inspect the tree to make sure we swapped + VERIFY_ARE_EQUAL(fourthId, tab->_rootPane->_secondChild->_secondChild->Id().value()); + VERIFY_ARE_EQUAL(thirdId, tab->_rootPane->_secondChild->_firstChild->Id().value()); + }); + } + void TabTests::NextMRUTab() { // This is a test for GH#8025 - we want to make sure that we can do both @@ -1342,4 +1552,55 @@ namespace TerminalAppLocalTests }); } + void TabTests::TestClampSwitchToTab() + { + Log::Comment(L"Test that switching to a tab index higher than the number of tabs just clamps to the last tab."); + + auto page = _commonSetup(); + VERIFY_IS_NOT_NULL(page); + + Log::Comment(L"Create a second tab"); + TestOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 1 }; + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(2u, page->_tabs.Size()); + + Log::Comment(L"Create a third tab"); + TestOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 2 }; + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(3u, page->_tabs.Size()); + + TestOnUIThread([&page]() { + auto focusedTabIndexOpt{ page->_GetFocusedTabIndex() }; + VERIFY_IS_TRUE(focusedTabIndexOpt.has_value()); + VERIFY_ARE_EQUAL(2u, focusedTabIndexOpt.value()); + }); + + TestOnUIThread([&page]() { + Log::Comment(L"Switch to the first tab"); + page->_SelectTab(0); + }); + + TestOnUIThread([&page]() { + auto focusedTabIndexOpt{ page->_GetFocusedTabIndex() }; + + VERIFY_IS_TRUE(focusedTabIndexOpt.has_value()); + VERIFY_ARE_EQUAL(0u, focusedTabIndexOpt.value()); + }); + + TestOnUIThread([&page]() { + Log::Comment(L"Switch to the tab 6, which is greater than number of tabs. This should switch to the third tab"); + page->_SelectTab(6); + }); + + TestOnUIThread([&page]() { + auto focusedTabIndexOpt{ page->_GetFocusedTabIndex() }; + VERIFY_IS_TRUE(focusedTabIndexOpt.has_value()); + VERIFY_ARE_EQUAL(2u, focusedTabIndexOpt.value()); + }); + } + } diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj index a6340764b..25478e4fb 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj @@ -92,11 +92,11 @@ x86 $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.2.5.0-prerelease.201202003\runtimes\win10-$(Native-Platform)\native\" + <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.2.6.2-prerelease.210818003\runtimes\win10-$(Native-Platform)\native\" - + diff --git a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj index cef96d52f..84e0ebf04 100644 --- a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj @@ -123,7 +123,7 @@ - + diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index f65422d3f..05f987661 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -28,8 +28,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation CATCH_LOG(); } - // This is a private constructor to be used in unit tests, where we don't - // want each Monarch to necessarily use the current PID. + // This constructor is intended to be used in unit tests, + // but we need to make it public in order to use make_self + // in the tests. It's not exposed through the idl though + // so it's not _truly_ fully public which should be acceptable. Monarch::Monarch(const uint64_t testPID) : _ourPID{ testPID } { @@ -78,6 +80,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation peasant.IdentifyWindowsRequested({ this, &Monarch::_identifyWindows }); peasant.RenameRequested({ this, &Monarch::_renameRequested }); + peasant.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequestedHandlers(*this, nullptr); }); + peasant.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequestedHandlers(*this, nullptr); }); + _peasants[newPeasantsId] = peasant; TraceLoggingWrite(g_hRemotingProvider, @@ -201,6 +206,12 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _clearOldMruEntries(id); } + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_lookupPeasantIdForName", + TraceLoggingWideString(std::wstring{ name }.c_str(), "name", "the name we're looking for"), + TraceLoggingUInt64(result, "peasantID", "the ID of the peasant with that name"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); return result; } @@ -732,24 +743,55 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation try { args.FoundMatch(false); + + // If a WindowID is provided from the args, use that first. uint64_t windowId = 0; - // If no name was provided, then just summon the MRU window. - if (searchedForName.empty()) + if (args.WindowID()) { - // Use the value of the `desktop` arg to determine if we should - // limit to the current desktop (desktop:onCurrent) or not - // (desktop:any or desktop:toCurrent) - windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false); + windowId = args.WindowID().Value(); } else { - // Try to find a peasant that currently has this name - windowId = _lookupPeasantIdForName(searchedForName); + // If no name was provided, then just summon the MRU window. + if (searchedForName.empty()) + { + // Use the value of the `desktop` arg to determine if we should + // limit to the current desktop (desktop:onCurrent) or not + // (desktop:any or desktop:toCurrent) + windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false); + } + else + { + // Try to find a peasant that currently has this name + windowId = _lookupPeasantIdForName(searchedForName); + } } + if (auto targetPeasant{ _getPeasant(windowId) }) { targetPeasant.Summon(args.SummonBehavior()); args.FoundMatch(true); + + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonWindow_Success", + TraceLoggingWideString(searchedForName.c_str(), "searchedForName", "The name of the window we tried to summon"), + TraceLoggingUInt64(windowId, "peasantID", "The id of the window we tried to summon"), + TraceLoggingBoolean(args.OnCurrentDesktop(), "OnCurrentDesktop", "true iff the window needs to be on the current virtual desktop"), + TraceLoggingBoolean(args.SummonBehavior().MoveToCurrentDesktop(), "MoveToCurrentDesktop", "if true, move the window to the current virtual desktop"), + TraceLoggingBoolean(args.SummonBehavior().ToggleVisibility(), "ToggleVisibility", "true if we should toggle the visibility of the window"), + TraceLoggingUInt32(args.SummonBehavior().DropdownDuration(), "DropdownDuration", "the duration to dropdown the window"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + else + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonWindow_NoPeasant", + TraceLoggingWideString(searchedForName.c_str(), "searchedForName", "The name of the window we tried to summon"), + TraceLoggingUInt64(windowId, "peasantID", "The id of the window we tried to summon"), + TraceLoggingBoolean(args.OnCurrentDesktop(), "OnCurrentDesktop", "true iff the window needs to be on the current virtual desktop"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } } catch (...) @@ -762,4 +804,56 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } } + + // Method Description: + // - This method creates a map of peasant IDs to peasant names + // while removing dead peasants. + // Arguments: + // - + // Return Value: + // - A map of peasant IDs to their names. + Windows::Foundation::Collections::IMapView Monarch::GetPeasantNames() + { + auto names = winrt::single_threaded_map(); + + std::vector peasantsToErase{}; + for (const auto& [id, p] : _peasants) + { + try + { + names.Insert(id, p.WindowName()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + peasantsToErase.push_back(id); + } + } + + // Remove the dead peasants we came across while iterating. + for (const auto& id : peasantsToErase) + { + _peasants.erase(id); + _clearOldMruEntries(id); + } + + return names.GetView(); + } + + void Monarch::SummonAllWindows() + { + auto callback = [](auto&& p, auto&& /*id*/) { + SummonWindowBehavior args{}; + args.ToggleVisibility(false); + p.Summon(args); + }; + auto onError = [](auto&& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonAll_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not summon"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + _forAllPeasantsIgnoringTheDead(callback, onError); + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 9dc8fc77e..2aa30dab4 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -41,6 +41,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation struct Monarch : public MonarchT { Monarch(); + Monarch(const uint64_t testPID); ~Monarch(); uint64_t GetPID(); @@ -51,10 +52,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void HandleActivatePeasant(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); + void SummonAllWindows(); + Windows::Foundation::Collections::IMapView GetPeasantNames(); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: - Monarch(const uint64_t testPID); uint64_t _ourPID; uint64_t _nextPeasantID{ 1 }; diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index d87d780b3..f4e8bd8b4 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -28,6 +28,7 @@ namespace Microsoft.Terminal.Remoting Boolean FoundMatch; SummonWindowBehavior SummonBehavior; + Windows.Foundation.IReference WindowID; } @@ -40,6 +41,11 @@ namespace Microsoft.Terminal.Remoting void HandleActivatePeasant(WindowActivatedArgs args); void SummonWindow(SummonWindowSelectionArgs args); + void SummonAllWindows(); + Windows.Foundation.Collections.IMapView GetPeasantNames { get; }; + event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; } diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index 58a71acba..a9787f597 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -20,8 +20,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { } - // This is a private constructor to be used in unit tests, where we don't - // want each Peasant to necessarily use the current PID. + // This constructor is intended to be used in unit tests, + // but we need to make it public in order to use make_self + // in the tests. It's not exposed through the idl though + // so it's not _truly_ fully public which should be acceptable. Peasant::Peasant(const uint64_t testPID) : _ourPID{ testPID } { @@ -31,6 +33,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { _id = id; } + uint64_t Peasant::GetID() { return _id; @@ -222,4 +225,36 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + + void Peasant::RequestShowTrayIcon() + { + try + { + _ShowTrayIconRequestedHandlers(*this, nullptr); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_RequestShowTrayIcon", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + + void Peasant::RequestHideTrayIcon() + { + try + { + _HideTrayIconRequestedHandlers(*this, nullptr); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_RequestHideTrayIcon", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index 7b5b44e81..6093cd835 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -28,6 +28,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void RequestIdentifyWindows(); void DisplayWindowId(); void RequestRename(const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); @@ -40,6 +42,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RenameRequestArgs); TYPED_EVENT(SummonRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: Peasant(const uint64_t testPID); diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index bc36e3f30..454d748cf 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -64,6 +64,8 @@ namespace Microsoft.Terminal.Remoting void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested void RequestRename(RenameRequestArgs args); // Tells us to raise a RenameRequested void Summon(SummonWindowBehavior behavior); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; @@ -71,6 +73,8 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler DisplayWindowIdRequested; event Windows.Foundation.TypedEventHandler RenameRequested; event Windows.Foundation.TypedEventHandler SummonRequested; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; [default_interface] runtimeclass Peasant : IPeasant diff --git a/src/cascadia/Remoting/SummonWindowSelectionArgs.h b/src/cascadia/Remoting/SummonWindowSelectionArgs.h index 2696925ef..efbf72a3b 100644 --- a/src/cascadia/Remoting/SummonWindowSelectionArgs.h +++ b/src/cascadia/Remoting/SummonWindowSelectionArgs.h @@ -34,6 +34,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation WINRT_PROPERTY(bool, FoundMatch, false); WINRT_PROPERTY(bool, OnCurrentDesktop, false); WINRT_PROPERTY(SummonWindowBehavior, SummonBehavior); + + WINRT_PROPERTY(Windows::Foundation::IReference, WindowID); }; } diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 88e0adbf0..a415bfd98 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -254,6 +254,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // window, and when the current monarch dies. _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); + _monarch.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequestedHandlers(*this, nullptr); }); + _monarch.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequestedHandlers(*this, nullptr); }); _BecameMonarchHandlers(*this, nullptr); } @@ -509,4 +511,57 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _monarch.SummonWindow(args); } + void WindowManager::SummonAllWindows() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + _monarch.SummonAllWindows(); + } + } + + Windows::Foundation::Collections::IMapView WindowManager::GetPeasantNames() + { + // We should only get called when we're the monarch since the monarch + // is the only one that knows about all peasants. + return _monarch.GetPeasantNames(); + } + + // Method Description: + // - Ask the monarch to show a tray icon. + // Arguments: + // - + // Return Value: + // - + winrt::fire_and_forget WindowManager::RequestShowTrayIcon() + { + co_await winrt::resume_background(); + _peasant.RequestShowTrayIcon(); + } + + // Method Description: + // - Ask the monarch to hide its tray icon. + // Arguments: + // - + // Return Value: + // - + winrt::fire_and_forget WindowManager::RequestHideTrayIcon() + { + auto strongThis{ get_strong() }; + co_await winrt::resume_background(); + _peasant.RequestHideTrayIcon(); + } + + bool WindowManager::DoesQuakeWindowExist() + { + const auto names = GetPeasantNames(); + for (const auto [id, name] : names) + { + if (name == QuakeWindowName) + { + return true; + } + } + return false; + } + } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 0b87075f9..8dd52f50d 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -40,8 +40,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation bool IsMonarch(); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); + void SummonAllWindows(); + Windows::Foundation::Collections::IMapView GetPeasantNames(); + + winrt::fire_and_forget RequestShowTrayIcon(); + winrt::fire_and_forget RequestHideTrayIcon(); + bool DoesQuakeWindowExist(); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: bool _shouldCreateWindow{ false }; diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index 547b96f9a..ca1f9f747 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -12,7 +12,14 @@ namespace Microsoft.Terminal.Remoting IPeasant CurrentWindow(); Boolean IsMonarch { get; }; void SummonWindow(SummonWindowSelectionArgs args); + void SummonAllWindows(); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); + Boolean DoesQuakeWindowExist(); + Windows.Foundation.Collections.IMapView GetPeasantNames(); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler BecameMonarch; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; } diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml index a0678e05e..8c456b6a9 100644 --- a/src/cascadia/TerminalApp/App.xaml +++ b/src/cascadia/TerminalApp/App.xaml @@ -20,7 +20,8 @@ - + + + + + diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index cdc92aaf6..ee62b60c8 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -143,6 +143,20 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleMovePane(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args == nullptr) + { + args.Handled(false); + } + else if (const auto& realArgs = args.ActionArgs().try_as()) + { + auto moved = _MovePane(realArgs.TabIndex()); + args.Handled(moved); + } + } + void TerminalPage::_HandleSplitPane(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -161,6 +175,13 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleToggleSplitOrientation(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + _ToggleSplitOrientation(); + args.Handled(true); + } + void TerminalPage::_HandleTogglePaneZoom(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -257,12 +278,12 @@ namespace winrt::TerminalApp::implementation { if (args == nullptr) { - _OpenNewTab(nullptr); + LOG_IF_FAILED(_OpenNewTab(nullptr)); args.Handled(true); } else if (const auto& realArgs = args.ActionArgs().try_as()) { - _OpenNewTab(realArgs.TerminalArgs()); + LOG_IF_FAILED(_OpenNewTab(realArgs.TerminalArgs())); args.Handled(true); } } @@ -307,8 +328,29 @@ namespace winrt::TerminalApp::implementation } else { - _MoveFocus(realArgs.FocusDirection()); - args.Handled(true); + // Mark as handled only when the move succeeded (e.g. when there + // is a pane to move to), otherwise mark as unhandled so the + // keychord can propagate to the terminal (GH#6129) + const auto moveSucceeded = _MoveFocus(realArgs.FocusDirection()); + args.Handled(moveSucceeded); + } + } + } + + void TerminalPage::_HandleSwapPane(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + if (realArgs.Direction() == FocusDirection::None) + { + // Do nothing + args.Handled(false); + } + else + { + auto swapped = _SwapPane(realArgs.Direction()); + args.Handled(swapped); } } } @@ -711,11 +753,10 @@ namespace winrt::TerminalApp::implementation newTerminalArgs = NewTerminalArgs(); } - const auto profileGuid{ _settings.GetProfileForArgs(newTerminalArgs) }; - const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) }; + const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) }; // Manually fill in the evaluated profile. - newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profileGuid)); + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); _OpenNewWindow(false, newTerminalArgs); actionArgs.Handled(true); } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index f0545654c..ae8bced53 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -192,6 +192,8 @@ void AppCommandlineArgs::_buildParser() _buildSplitPaneParser(); _buildFocusTabParser(); _buildMoveFocusParser(); + _buildMovePaneParser(); + _buildSwapPaneParser(); _buildFocusPaneParser(); } @@ -296,6 +298,43 @@ void AppCommandlineArgs::_buildSplitPaneParser() setupSubcommand(_newPaneCommand); setupSubcommand(_newPaneShort); } +// Method Description: +// - Adds the `move-pane` subcommand and related options to the commandline parser. +// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane` +// Arguments: +// - +// Return Value: +// - +void AppCommandlineArgs::_buildMovePaneParser() +{ + _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc")); + _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc")); + + auto setupSubcommand = [this](auto* subcommand) { + subcommand->add_option("-t,--tab", + _movePaneTabIndex, + RS_A(L"CmdMovePaneTabArgDesc")); + + // When ParseCommand is called, if this subcommand was provided, this + // callback function will be triggered on the same thread. We can be sure + // that `this` will still be safe - this function just lets us know this + // command was parsed. + subcommand->callback([&, this]() { + // Build the action from the values we've parsed on the commandline. + ActionAndArgs movePaneAction{}; + + if (_movePaneTabIndex >= 0) + { + movePaneAction.Action(ShortcutAction::MovePane); + MovePaneArgs args{ static_cast(_movePaneTabIndex) }; + movePaneAction.Args(args); + _startupActions.push_back(movePaneAction); + } + }); + }; + setupSubcommand(_movePaneCommand); + setupSubcommand(_movePaneShort); +} // Method Description: // - Adds the `focus-tab` subcommand and related options to the commandline parser. @@ -341,6 +380,11 @@ void AppCommandlineArgs::_buildFocusTabParser() else if (_focusNextTab || _focusPrevTab) { focusTabAction.Action(_focusNextTab ? ShortcutAction::NextTab : ShortcutAction::PrevTab); + // GH#10070 - make sure to not use the MRU order when switching + // tabs on the commandline. That wouldn't make any sense! + focusTabAction.Args(_focusNextTab ? + static_cast(NextTabArgs(TabSwitcherMode::Disabled)) : + static_cast(PrevTabArgs(TabSwitcherMode::Disabled))); _startupActions.push_back(std::move(focusTabAction)); } }); @@ -350,6 +394,15 @@ void AppCommandlineArgs::_buildFocusTabParser() setupSubcommand(_focusTabShort); } +static const std::map focusDirectionMap = { + { "left", FocusDirection::Left }, + { "right", FocusDirection::Right }, + { "up", FocusDirection::Up }, + { "down", FocusDirection::Down }, + { "nextInOrder", FocusDirection::NextInOrder }, + { "previousInOrder", FocusDirection::PreviousInOrder }, +}; + // Method Description: // - Adds the `move-focus` subcommand and related options to the commandline parser. // - Additionally adds the `mf` subcommand, which is just a shortened version of `move-focus` @@ -363,18 +416,11 @@ void AppCommandlineArgs::_buildMoveFocusParser() _moveFocusShort = _app.add_subcommand("mf", RS_A(L"CmdMFDesc")); auto setupSubcommand = [this](auto* subcommand) { - std::map map = { - { "left", FocusDirection::Left }, - { "right", FocusDirection::Right }, - { "up", FocusDirection::Up }, - { "down", FocusDirection::Down } - }; - auto* directionOpt = subcommand->add_option("direction", _moveFocusDirection, RS_A(L"CmdMoveFocusDirectionArgDesc")); - directionOpt->transform(CLI::CheckedTransformer(map, CLI::ignore_case)); + directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case)); directionOpt->required(); // When ParseCommand is called, if this subcommand was provided, this // callback function will be triggered on the same thread. We can be sure @@ -398,6 +444,44 @@ void AppCommandlineArgs::_buildMoveFocusParser() setupSubcommand(_moveFocusShort); } +// Method Description: +// - Adds the `swap-pane` subcommand and related options to the commandline parser. +// Arguments: +// - +// Return Value: +// - +void AppCommandlineArgs::_buildSwapPaneParser() +{ + _swapPaneCommand = _app.add_subcommand("swap-pane", RS_A(L"CmdSwapPaneDesc")); + + auto setupSubcommand = [this](auto* subcommand) { + auto* directionOpt = subcommand->add_option("direction", + _swapPaneDirection, + RS_A(L"CmdSwapPaneDirectionArgDesc")); + + directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case)); + directionOpt->required(); + // When ParseCommand is called, if this subcommand was provided, this + // callback function will be triggered on the same thread. We can be sure + // that `this` will still be safe - this function just lets us know this + // command was parsed. + subcommand->callback([&, this]() { + if (_swapPaneDirection != FocusDirection::None) + { + SwapPaneArgs args{ _swapPaneDirection }; + + ActionAndArgs actionAndArgs{}; + actionAndArgs.Action(ShortcutAction::SwapPane); + actionAndArgs.Args(args); + + _startupActions.push_back(std::move(actionAndArgs)); + } + }); + }; + + setupSubcommand(_swapPaneCommand); +} + // Method Description: // - Adds the `focus-pane` subcommand and related options to the commandline parser. // - Additionally adds the `fp` subcommand, which is just a shortened version of `focus-pane` @@ -519,6 +603,13 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT args.Profile(winrt::to_hstring(_profileName)); } + if (!*subcommand.profileNameOption && !_commandline.empty()) + { + // If there's no profile, but there IS a command line, set the tab title to the first part of the command + // This will ensure that the tab we spawn has a name (since it didn't get one from its profile!) + args.TabTitle(winrt::to_hstring(til::at(_commandline, 0))); + } + if (*subcommand.startingDirectoryOption) { args.StartingDirectory(winrt::to_hstring(_startingDirectory)); @@ -574,6 +665,9 @@ bool AppCommandlineArgs::_noCommandsProvided() *_focusTabShort || *_moveFocusCommand || *_moveFocusShort || + *_movePaneCommand || + *_movePaneShort || + *_swapPaneCommand || *_focusPaneCommand || *_focusPaneShort || *_newPaneShort.subcommand || @@ -602,11 +696,13 @@ void AppCommandlineArgs::_resetStateToDefault() _splitPaneSize = 0.5f; _splitDuplicate = false; + _movePaneTabIndex = -1; _focusTabIndex = -1; _focusNextTab = false; _focusPrevTab = false; _moveFocusDirection = FocusDirection::None; + _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 17593c156..598c3b8ed 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -82,6 +82,9 @@ private: CLI::App* _focusTabShort; CLI::App* _moveFocusCommand; CLI::App* _moveFocusShort; + CLI::App* _movePaneCommand; + CLI::App* _movePaneShort; + CLI::App* _swapPaneCommand; CLI::App* _focusPaneCommand; CLI::App* _focusPaneShort; @@ -95,6 +98,7 @@ private: bool _suppressApplicationTitle{ false }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; + winrt::Microsoft::Terminal::Settings::Model::FocusDirection _swapPaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; // _commandline will contain the command line with which we'll be spawning a new terminal std::vector _commandline; @@ -104,6 +108,7 @@ private: bool _splitDuplicate{ false }; float _splitPaneSize{ 0.5f }; + int _movePaneTabIndex{ -1 }; int _focusTabIndex{ -1 }; bool _focusNextTab{ false }; bool _focusPrevTab{ false }; @@ -128,6 +133,8 @@ private: void _buildSplitPaneParser(); void _buildFocusTabParser(); void _buildMoveFocusParser(); + void _buildMovePaneParser(); + void _buildSwapPaneParser(); void _buildFocusPaneParser(); bool _noCommandsProvided(); void _resetStateToDefault(); diff --git a/src/cascadia/TerminalApp/AppKeyBindings.cpp b/src/cascadia/TerminalApp/AppKeyBindings.cpp index 71a3a03bb..3f921bc55 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindings.cpp @@ -21,6 +21,11 @@ namespace winrt::TerminalApp::implementation return false; } + bool AppKeyBindings::IsKeyChordExplicitlyUnbound(const KeyChord& kc) + { + return _actionMap.IsKeyChordExplicitlyUnbound(kc); + } + void AppKeyBindings::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch) { _dispatch = dispatch; diff --git a/src/cascadia/TerminalApp/AppKeyBindings.h b/src/cascadia/TerminalApp/AppKeyBindings.h index 5004edeca..f4f6c20ba 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.h +++ b/src/cascadia/TerminalApp/AppKeyBindings.h @@ -20,6 +20,7 @@ namespace winrt::TerminalApp::implementation AppKeyBindings() = default; bool TryKeyChord(winrt::Microsoft::Terminal::Control::KeyChord const& kc); + bool IsKeyChordExplicitlyUnbound(winrt::Microsoft::Terminal::Control::KeyChord const& kc); void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch); void SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap); diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 76ff21745..106da0fbd 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -203,12 +203,16 @@ namespace winrt::TerminalApp::implementation _isElevated = _isUserAdmin(); _root = winrt::make_self(); - _reloadSettings = std::make_shared>(_root->Dispatcher(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() { + _reloadSettings = std::make_shared>(winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() { if (auto self{ weakSelf.get() }) { self->_ReloadSettings(); } }); + + _languageProfileNotifier = winrt::make_self([this]() { + _reloadSettings->Run(); + }); } // Method Description: @@ -1125,28 +1129,11 @@ namespace winrt::TerminalApp::implementation } } - // Method Description: - // - Gets the taskbar state value from the last active control - // Return Value: - // - The taskbar state of the last active control - size_t AppLogic::GetLastActiveControlTaskbarState() + winrt::TerminalApp::TaskbarState AppLogic::TaskbarState() { if (_root) { - return _root->GetLastActiveControlTaskbarState(); - } - return {}; - } - - // Method Description: - // - Gets the taskbar progress value from the last active control - // Return Value: - // - The taskbar progress of the last active control - size_t AppLogic::GetLastActiveControlTaskbarProgress() - { - if (_root) - { - return _root->GetLastActiveControlTaskbarProgress(); + return _root->TaskbarState(); } return {}; } @@ -1229,6 +1216,11 @@ namespace winrt::TerminalApp::implementation auto actions = winrt::single_threaded_vector(std::move(appArgs.GetStartupActions())); _root->ProcessStartupActions(actions, false, cwd); + + if (appArgs.IsHandoffListener()) + { + _root->SetInboundListener(true); + } } // Return the result of parsing with commandline, though it may or may not be used. return result; @@ -1450,4 +1442,39 @@ namespace winrt::TerminalApp::implementation return _root->IsQuakeWindow(); } + bool AppLogic::GetMinimizeToTray() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings.GlobalSettings().MinimizeToTray(); + } + else + { + return false; + } + } + + bool AppLogic::GetAlwaysShowTrayIcon() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings.GlobalSettings().AlwaysShowTrayIcon(); + } + else + { + return false; + } + } } diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index dfbe27a44..dbaf301c3 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -5,8 +5,9 @@ #include "AppLogic.g.h" #include "FindTargetWindowResult.g.h" -#include "TerminalPage.h" #include "Jumplist.h" +#include "LanguageProfileNotifier.h" +#include "TerminalPage.h" #include #include @@ -89,8 +90,10 @@ namespace winrt::TerminalApp::implementation void WindowCloseButtonClicked(); - size_t GetLastActiveControlTaskbarState(); - size_t GetLastActiveControlTaskbarProgress(); + winrt::TerminalApp::TaskbarState TaskbarState(); + + bool GetMinimizeToTray(); + bool GetAlwaysShowTrayIcon(); winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); @@ -110,12 +113,8 @@ namespace winrt::TerminalApp::implementation // 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 }; - Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; - wil::unique_folder_change_reader_nothrow _reader; - std::shared_ptr> _reloadSettings; - til::throttled_func_trailing<> _reloadState; winrt::hstring _settingsLoadExceptionText; HRESULT _settingsLoadedResult = S_OK; bool _loadedInitialSettings = false; @@ -124,6 +123,15 @@ namespace winrt::TerminalApp::implementation ::TerminalApp::AppCommandlineArgs _appArgs; ::TerminalApp::AppCommandlineArgs _settingsAppArgs; + + std::shared_ptr> _reloadSettings; + til::throttled_func_trailing<> _reloadState; + + // These fields invoke _reloadSettings and must be destroyed before _reloadSettings. + // (C++ destroys members in reverse-declaration-order.) + winrt::com_ptr _languageProfileNotifier; + wil::unique_folder_change_reader_nothrow _reader; + static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view args, const Microsoft::Terminal::Settings::Model::WindowingMode& windowingBehavior); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index bb334d26f..1f7675ad4 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -68,8 +68,10 @@ namespace TerminalApp void TitlebarClicked(); void WindowCloseButtonClicked(); - UInt64 GetLastActiveControlTaskbarState(); - UInt64 GetLastActiveControlTaskbarProgress(); + TaskbarState TaskbarState{ get; }; + + Boolean GetMinimizeToTray(); + Boolean GetAlwaysShowTrayIcon(); FindTargetWindowResult FindTargetWindow(String[] args); diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp index 651158122..bd6badf65 100644 --- a/src/cascadia/TerminalApp/CommandPalette.cpp +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -250,10 +250,12 @@ namespace winrt::TerminalApp::implementation void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) { - auto key = e.OriginalKey(); - auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); - auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); - auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + const auto key = e.OriginalKey(); + const auto scanCode = e.KeyStatus().ScanCode; + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); // Some keypresses such as Tab, Return, Esc, and Arrow Keys are ignored by controls because // they're not considered input key presses. While they don't raise KeyDown events, @@ -264,7 +266,7 @@ namespace winrt::TerminalApp::implementation // a really widely used keyboard navigation key. if (_currentMode == CommandPaletteMode::TabSwitchMode && _actionMap) { - winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, static_cast(key) }; + winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast(key), static_cast(scanCode) }; if (const auto cmd{ _actionMap.GetActionByKeyChord(kc) }) { if (cmd.ActionAndArgs().Action() == ShortcutAction::PrevTab) @@ -402,9 +404,10 @@ namespace winrt::TerminalApp::implementation // - void CommandPalette::_anchorKeyUpHandler() { - auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); - auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); - auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); if (!ctrlDown && !altDown && !shiftDown) { diff --git a/src/cascadia/TerminalApp/DebugTapConnection.cpp b/src/cascadia/TerminalApp/DebugTapConnection.cpp index 0a71c8bff..54f327b3e 100644 --- a/src/cascadia/TerminalApp/DebugTapConnection.cpp +++ b/src/cascadia/TerminalApp/DebugTapConnection.cpp @@ -19,6 +19,7 @@ namespace winrt::Microsoft::TerminalApp::implementation _wrappedConnection{ std::move(wrappedConnection) } { } + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) {} ~DebugInputTapConnection() = default; void Start() { diff --git a/src/cascadia/TerminalApp/DebugTapConnection.h b/src/cascadia/TerminalApp/DebugTapConnection.h index 16e3fc82e..c5156afc4 100644 --- a/src/cascadia/TerminalApp/DebugTapConnection.h +++ b/src/cascadia/TerminalApp/DebugTapConnection.h @@ -13,6 +13,7 @@ namespace winrt::Microsoft::TerminalApp::implementation { public: explicit DebugTapConnection(Microsoft::Terminal::TerminalConnection::ITerminalConnection wrappedConnection); + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/){}; ~DebugTapConnection(); void Start(); void WriteInput(hstring const& data); diff --git a/src/cascadia/TerminalApp/LanguageProfileNotifier.cpp b/src/cascadia/TerminalApp/LanguageProfileNotifier.cpp new file mode 100644 index 000000000..9655947a4 --- /dev/null +++ b/src/cascadia/TerminalApp/LanguageProfileNotifier.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "LanguageProfileNotifier.h" + +using namespace winrt::TerminalApp::implementation; + +LanguageProfileNotifier::LanguageProfileNotifier(std::function&& callback) : + _callback{ std::move(callback) }, + _currentKeyboardLayout{ GetKeyboardLayout(0) } +{ + const auto manager = wil::CoCreateInstance(CLSID_TF_ThreadMgr); + _source = manager.query(); + if (FAILED(_source->AdviseSink(IID_ITfInputProcessorProfileActivationSink, static_cast(this), &_cookie))) + { + _cookie = TF_INVALID_COOKIE; + THROW_LAST_ERROR(); + } +} + +LanguageProfileNotifier::~LanguageProfileNotifier() +{ + if (_cookie != TF_INVALID_COOKIE) + { + _source->UnadviseSink(_cookie); + } +} + +STDMETHODIMP LanguageProfileNotifier::OnActivated(DWORD /*dwProfileType*/, LANGID /*langid*/, REFCLSID /*clsid*/, REFGUID /*catid*/, REFGUID /*guidProfile*/, HKL hkl, DWORD /*dwFlags*/) +{ + if (hkl && hkl != _currentKeyboardLayout) + { + _currentKeyboardLayout = hkl; + try + { + _callback(); + } + CATCH_RETURN(); + } + return S_OK; +} diff --git a/src/cascadia/TerminalApp/LanguageProfileNotifier.h b/src/cascadia/TerminalApp/LanguageProfileNotifier.h new file mode 100644 index 000000000..865330455 --- /dev/null +++ b/src/cascadia/TerminalApp/LanguageProfileNotifier.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace winrt::TerminalApp::implementation +{ + class LanguageProfileNotifier : public winrt::implements + { + public: + explicit LanguageProfileNotifier(std::function&& callback); + ~LanguageProfileNotifier(); + STDMETHODIMP OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID clsid, REFGUID catid, REFGUID guidProfile, HKL hkl, DWORD dwFlags); + + private: + std::function _callback; + wil::com_ptr _source; + DWORD _cookie = TF_INVALID_COOKIE; + HKL _currentKeyboardLayout; + }; +} diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index cb8f4278a..75def5f58 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -34,7 +34,7 @@ static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Wi winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_focusedBorderBrush = { nullptr }; winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_unfocusedBorderBrush = { nullptr }; -Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocused) : +Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFocused) : _control{ control }, _lastActive{ lastFocused }, _profile{ profile } @@ -214,94 +214,507 @@ bool Pane::ResizePane(const ResizeDirection& direction) } // Method Description: -// - Attempts to handle moving focus to one of our children. If our split -// direction isn't appropriate for the move direction, then we'll return -// false, to try and let our parent handle the move. If our child we'd move -// focus to is already focused, we'll also return false, to again let our -// parent try and handle the focus movement. +// - Attempt to navigate from the sourcePane according to direction. +// - If the direction is NextInOrder or PreviousInOrder, the next or previous +// leaf in the tree, respectively, will be returned. +// - If the direction is {Up, Down, Left, Right} then the visually-adjacent +// neighbor (if it exists) will be returned. If there are multiple options +// then the first-most leaf will be selected. // Arguments: -// - direction: The direction to move the focus in. +// - sourcePane: the pane to navigate from +// - direction: which direction to go in // Return Value: -// - true if we handled this focus move request. -bool Pane::_NavigateFocus(const FocusDirection& direction) +// - The result of navigating from source according to direction, which may be +// nullptr (i.e. no pane was found in that direction). +std::shared_ptr Pane::NavigateDirection(const std::shared_ptr sourcePane, const FocusDirection& direction) { + // Can't navigate anywhere if we are a leaf + if (_IsLeaf()) + { + return nullptr; + } + + // If the MRU previous pane is requested we can't move; the tab handles MRU + if (direction == FocusDirection::None || direction == FocusDirection::Previous) + { + return nullptr; + } + + // Check if we in-order traversal is requested + if (direction == FocusDirection::NextInOrder) + { + return NextPane(sourcePane); + } + + if (direction == FocusDirection::PreviousInOrder) + { + return PreviousPane(sourcePane); + } + + // We are left with directional traversal now + // If the focus direction does not match the split direction, the source pane + // and its neighbor must necessarily be contained within the same child. if (!DirectionMatchesSplit(direction, _splitState)) { - return false; + if (auto p = _firstChild->NavigateDirection(sourcePane, direction)) + { + return p; + } + return _secondChild->NavigateDirection(sourcePane, direction); } - const bool focusSecond = (direction == FocusDirection::Right) || (direction == FocusDirection::Down); + // Since the direction is the same as our split, it is possible that we must + // move focus from from one child to another child. + // We now must keep track of state while we recurse. + const auto paneNeighborPair = _FindPaneAndNeighbor(sourcePane, direction, { 0, 0 }); - const auto newlyFocusedChild = focusSecond ? _secondChild : _firstChild; - - // If the child we want to move focus to is _already_ focused, return false, - // to try and let our parent figure it out. - if (newlyFocusedChild->_HasFocusedChild()) + if (paneNeighborPair.source && paneNeighborPair.neighbor) { - return false; + return paneNeighborPair.neighbor; } - // Transfer focus to our child, and update the focus of our tree. - newlyFocusedChild->_FocusFirstChild(); - UpdateVisuals(); - - return true; + return nullptr; } // Method Description: -// - Attempts to move focus to one of our children. If we have a focused child, -// we'll try to move the focus in the direction requested. -// - If there isn't a pane that exists as a child of this pane in the correct -// direction, we'll return false. This will indicate to our parent that they -// should try and move the focus themselves. In this way, the focus can move -// up and down the tree to the correct pane. -// - This method is _very_ similar to ResizePane. Both are trying to find the -// right separator to move (focus) in a direction. +// - Attempts to find the succeeding pane of the provided pane. +// - NB: If targetPane is not a leaf, then this will return one of its children. // Arguments: -// - direction: The direction to move the focus in. +// - targetPane: The pane to search for. // Return Value: -// - true if we or a child handled this focus move request. -bool Pane::NavigateFocus(const FocusDirection& direction) +// - The next pane in tree order after the target pane (if found) +std::shared_ptr Pane::NextPane(const std::shared_ptr targetPane) { - // If we're a leaf, do nothing. We can't possibly have a descendant with a - // separator the correct direction. + // if we are a leaf pane there is no next pane. if (_IsLeaf()) + { + return nullptr; + } + + std::shared_ptr firstLeaf = nullptr; + std::shared_ptr nextPane = nullptr; + bool foundTarget = false; + + auto foundNext = WalkTree([&](auto pane) { + // In case the target pane is the last pane in the tree, keep a reference + // to the first leaf so we can wrap around. + if (firstLeaf == nullptr && pane->_IsLeaf()) + { + firstLeaf = pane; + } + + // If we've found the target pane already, get the next leaf pane. + if (foundTarget && pane->_IsLeaf()) + { + nextPane = pane; + return true; + } + + // Test if we're the target pane so we know to return the next pane. + if (pane == targetPane) + { + foundTarget = true; + } + + return false; + }); + + // If we found the desired pane just return it + if (foundNext) + { + return nextPane; + } + + // If we found the target pane, but not the next pane it means we were the + // last leaf in the tree. + if (foundTarget) + { + return firstLeaf; + } + + return nullptr; +} + +// Method Description: +// - Attempts to find the preceding pane of the provided pane. +// Arguments: +// - targetPane: The pane to search for. +// Return Value: +// - The previous pane in tree order before the target pane (if found) +std::shared_ptr Pane::PreviousPane(const std::shared_ptr targetPane) +{ + // if we are a leaf pane there is no previous pane. + if (_IsLeaf()) + { + return nullptr; + } + + std::shared_ptr lastLeaf = nullptr; + bool foundTarget = false; + + WalkTree([&](auto pane) { + if (pane == targetPane) + { + foundTarget = true; + // If we were not the first leaf, then return the previous leaf. + // Otherwise keep walking the tree to get the last pane. + if (lastLeaf != nullptr) + { + return true; + } + } + + if (pane->_IsLeaf()) + { + lastLeaf = pane; + } + + return false; + }); + + // If we found the target pane then lastLeaf will either be the preceding + // pane or the last pane in the tree if targetPane is the first leaf. + if (foundTarget) + { + return lastLeaf; + } + + return nullptr; +} + +// Method Description: +// - Attempts to find the parent pane of the provided pane. +// Arguments: +// - pane: The pane to search for. +// Return Value: +// - the parent of `pane` if pane is in this tree. +std::shared_ptr Pane::_FindParentOfPane(const std::shared_ptr pane) +{ + if (_IsLeaf()) + { + return nullptr; + } + + if (_firstChild == pane || _secondChild == pane) + { + return shared_from_this(); + } + + if (auto p = _firstChild->_FindParentOfPane(pane)) + { + return p; + } + + return _secondChild->_FindParentOfPane(pane); +} + +// Method Description: +// - Attempts to swap the location of the two given panes in the tree. +// Searches the tree starting at this pane to find the parent pane for each of +// the arguments, and if both parents are found, replaces the appropriate +// child in each. +// Arguments: +// - first: A pointer to the first pane to switch. +// - second: A pointer to the second pane to switch. +// Return Value: +// - true if a swap was performed. +bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) +{ + // If there is nothing to swap, just return. + if (first == second || _IsLeaf()) { return false; } - // Check if either our first or second child is the currently focused leaf. - // If it is, and the requested move direction matches our separator, then - // we're the pane that needs to handle this focus move. - const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; - const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; - if (firstIsFocused || secondIsFocused) - { - return _NavigateFocus(direction); - } + std::unique_lock lock{ _createCloseLock }; - // If neither of our children were the focused leaf, then recurse into - // our children and see if they can handle the focus move. - // For each child, if it has a focused descendant, try having that child - // handle the focus move. - // If the child wasn't able to handle the focus move, it's possible that - // there were no descendants with a separator the correct direction. If - // our separator _is_ the correct direction, then we should be the pane - // to move focus into our other child. Otherwise, just return false, as - // we couldn't handle it either. - if ((!_firstChild->_IsLeaf()) && _firstChild->_HasFocusedChild()) - { - return _firstChild->NavigateFocus(direction) || _NavigateFocus(direction); - } + // Recurse through the tree to find the parent panes of each pane that is + // being swapped. + std::shared_ptr firstParent = _FindParentOfPane(first); + std::shared_ptr secondParent = _FindParentOfPane(second); - if ((!_secondChild->_IsLeaf()) && _secondChild->_HasFocusedChild()) + // We should have found either no elements, or both elements. + // If we only found one parent then the pane SwapPane was called on did not + // contain both panes as leaves, as could happen if the tree was modified + // after the pointers were found but before we reached this function. + if (firstParent && secondParent) { - return _secondChild->NavigateFocus(direction) || _NavigateFocus(direction); + // Swap size/display information of the two panes. + std::swap(first->_borders, second->_borders); + + // Replace the old child with new one, and revoke appropriate event + // handlers. + auto replaceChild = [](auto& parent, auto oldChild, auto newChild) { + // Revoke the old handlers + if (parent->_firstChild == oldChild) + { + parent->_firstChild->Closed(parent->_firstClosedToken); + parent->_firstChild = newChild; + } + else if (parent->_secondChild == oldChild) + { + parent->_secondChild->Closed(parent->_secondClosedToken); + parent->_secondChild = newChild; + } + // Clear now to ensure that we can add the child's grid to us later + parent->_root.Children().Clear(); + }; + + // Make sure that the right event handlers are set, and the children + // are placed in the appropriate locations in the grid. + auto updateParent = [](auto& parent) { + parent->_SetupChildCloseHandlers(); + parent->_root.Children().Clear(); + parent->_root.Children().Append(parent->_firstChild->GetRootElement()); + parent->_root.Children().Append(parent->_secondChild->GetRootElement()); + // Make sure they have the correct borders, and also that they are + // placed in the right location in the grid. + // This mildly reproduces ApplySplitDefinitions, but is different in + // that it does not want to utilize the parent's border to set child + // borders. + if (parent->_splitState == SplitState::Vertical) + { + Controls::Grid::SetColumn(parent->_firstChild->GetRootElement(), 0); + Controls::Grid::SetColumn(parent->_secondChild->GetRootElement(), 1); + } + else if (parent->_splitState == SplitState::Horizontal) + { + Controls::Grid::SetRow(parent->_firstChild->GetRootElement(), 0); + Controls::Grid::SetRow(parent->_secondChild->GetRootElement(), 1); + } + parent->_firstChild->_UpdateBorders(); + parent->_secondChild->_UpdateBorders(); + }; + + // If the firstParent and secondParent are the same, then we are just + // swapping the first child and second child of that parent. + if (firstParent == secondParent) + { + firstParent->_firstChild->Closed(firstParent->_firstClosedToken); + firstParent->_secondChild->Closed(firstParent->_secondClosedToken); + std::swap(firstParent->_firstChild, firstParent->_secondChild); + + updateParent(firstParent); + } + else + { + // Replace both children before updating display to ensure + // that the grid elements are not attached to multiple panes + replaceChild(firstParent, first, second); + replaceChild(secondParent, second, first); + updateParent(firstParent); + updateParent(secondParent); + } + + // For now the first pane is always the focused pane, so re-focus to + // make sure the cursor is still in the terminal since the root was moved. + first->_FocusFirstChild(); + + return true; } return false; } +// Method Description: +// - Given two panes, test whether the `direction` side of first is adjacent to second. +// Arguments: +// - first: The reference pane. +// - second: the pane to test adjacency with. +// - direction: The direction to search in from the reference pane. +// Return Value: +// - true if the two panes are adjacent. +bool Pane::_IsAdjacent(const std::shared_ptr first, + const Pane::PanePoint firstOffset, + const std::shared_ptr second, + const Pane::PanePoint secondOffset, + const FocusDirection& direction) const +{ + // Since float equality is tricky (arithmetic is non-associative, commutative), + // test if the two numbers are within an epsilon distance of each other. + auto floatEqual = [](float left, float right) { + return abs(left - right) < 1e-4F; + }; + + // When checking containment in a range, the range is half-closed, i.e. [x, x+w). + // If the direction is left test that the left side of the first element is + // next to the right side of the second element, and that the top left + // corner of the first element is within the second element's height + if (direction == FocusDirection::Left) + { + auto sharesBorders = floatEqual(firstOffset.x, secondOffset.x + gsl::narrow_cast(second->GetRootElement().ActualWidth())); + auto withinHeight = (firstOffset.y >= secondOffset.y) && (firstOffset.y < secondOffset.y + gsl::narrow_cast(second->GetRootElement().ActualHeight())); + + return sharesBorders && withinHeight; + } + // If the direction is right test that the right side of the first element is + // next to the left side of the second element, and that the top left + // corner of the first element is within the second element's height + else if (direction == FocusDirection::Right) + { + auto sharesBorders = floatEqual(firstOffset.x + gsl::narrow_cast(first->GetRootElement().ActualWidth()), secondOffset.x); + auto withinHeight = (firstOffset.y >= secondOffset.y) && (firstOffset.y < secondOffset.y + gsl::narrow_cast(second->GetRootElement().ActualHeight())); + + return sharesBorders && withinHeight; + } + // If the direction is up test that the top side of the first element is + // next to the bottom side of the second element, and that the top left + // corner of the first element is within the second element's width + else if (direction == FocusDirection::Up) + { + auto sharesBorders = floatEqual(firstOffset.y, secondOffset.y + gsl::narrow_cast(second->GetRootElement().ActualHeight())); + auto withinWidth = (firstOffset.x >= secondOffset.x) && (firstOffset.x < secondOffset.x + gsl::narrow_cast(second->GetRootElement().ActualWidth())); + + return sharesBorders && withinWidth; + } + // If the direction is down test that the bottom side of the first element is + // next to the top side of the second element, and that the top left + // corner of the first element is within the second element's width + else if (direction == FocusDirection::Down) + { + auto sharesBorders = floatEqual(firstOffset.y + gsl::narrow_cast(first->GetRootElement().ActualHeight()), secondOffset.y); + auto withinWidth = (firstOffset.x >= secondOffset.x) && (firstOffset.x < secondOffset.x + gsl::narrow_cast(second->GetRootElement().ActualWidth())); + + return sharesBorders && withinWidth; + } + return false; +} + +// Method Description: +// - Given the source pane, and its relative position in the tree, attempt to +// find its visual neighbor within the current pane's tree. +// The neighbor, if it exists, will be a leaf pane. +// Arguments: +// - direction: The direction to search in from the source pane. +// - searchResult: the source pane and its relative position. +// - sourceIsSecondSide: If the source pane is on the "second" side (down/right of split) +// relative to the branch being searched +// - offset: the offset of the current pane +// Return Value: +// - A tuple of Panes, the first being the focused pane if found, and the second +// being the adjacent pane if it exists, and a bool that represents if the move +// goes out of bounds. +Pane::PaneNeighborSearch Pane::_FindNeighborForPane(const FocusDirection& direction, + PaneNeighborSearch searchResult, + const bool sourceIsSecondSide, + const Pane::PanePoint offset) +{ + // Test if the move will go out of boundaries. E.g. if the focus is already + // on the second child of some pane and it attempts to move right, there + // can't possibly be a neighbor to be found in the first child. + if ((sourceIsSecondSide && (direction == FocusDirection::Right || direction == FocusDirection::Down)) || + (!sourceIsSecondSide && (direction == FocusDirection::Left || direction == FocusDirection::Up))) + { + return searchResult; + } + + // If we are a leaf node test if we adjacent to the focus node + if (_IsLeaf()) + { + if (_IsAdjacent(searchResult.source, searchResult.sourceOffset, shared_from_this(), offset, direction)) + { + searchResult.neighbor = shared_from_this(); + } + return searchResult; + } + + auto firstOffset = offset; + auto secondOffset = offset; + // The second child has an offset depending on the split + if (_splitState == SplitState::Horizontal) + { + secondOffset.y += gsl::narrow_cast(_firstChild->GetRootElement().ActualHeight()); + } + else + { + secondOffset.x += gsl::narrow_cast(_firstChild->GetRootElement().ActualWidth()); + } + auto sourceNeighborSearch = _firstChild->_FindNeighborForPane(direction, searchResult, sourceIsSecondSide, firstOffset); + if (sourceNeighborSearch.neighbor) + { + return sourceNeighborSearch; + } + + return _secondChild->_FindNeighborForPane(direction, searchResult, sourceIsSecondSide, secondOffset); +} + +// Method Description: +// - Searches the tree to find the source pane, and if it exists, the +// visually adjacent pane by direction. +// Arguments: +// - sourcePane: The pane to find the neighbor of. +// - direction: The direction to search in from the focused pane. +// - offset: The offset, with the top-left corner being (0,0), that the current pane is relative to the root. +// Return Value: +// - The (partial) search result. If the search was successful, the pane and its neighbor will be returned. +// Otherwise, the neighbor will be null and the focus will be null/non-null if it was found. +Pane::PaneNeighborSearch Pane::_FindPaneAndNeighbor(const std::shared_ptr sourcePane, const FocusDirection& direction, const Pane::PanePoint offset) +{ + // If we are the source pane, return ourselves + if (this == sourcePane.get()) + { + return { shared_from_this(), nullptr, offset }; + } + + if (_IsLeaf()) + { + return { nullptr, nullptr, offset }; + } + + // Search the first child, which has no offset from the parent pane + auto firstOffset = offset; + auto secondOffset = offset; + // The second child has an offset depending on the split + if (_splitState == SplitState::Horizontal) + { + secondOffset.y += gsl::narrow_cast(_firstChild->GetRootElement().ActualHeight()); + } + else + { + secondOffset.x += gsl::narrow_cast(_firstChild->GetRootElement().ActualWidth()); + } + + auto sourceNeighborSearch = _firstChild->_FindPaneAndNeighbor(sourcePane, direction, firstOffset); + // If we have both the focus element and its neighbor, we are done + if (sourceNeighborSearch.source && sourceNeighborSearch.neighbor) + { + return sourceNeighborSearch; + } + // if we only found the focus, then we search the second branch for the + // neighbor. + if (sourceNeighborSearch.source) + { + // If we can possibly have both sides of a direction, check if the sibling has the neighbor + if (DirectionMatchesSplit(direction, _splitState)) + { + return _secondChild->_FindNeighborForPane(direction, sourceNeighborSearch, false, secondOffset); + } + return sourceNeighborSearch; + } + + // If we didn't find the focus at all, we need to search the second branch + // for the focus (and possibly its neighbor). + sourceNeighborSearch = _secondChild->_FindPaneAndNeighbor(sourcePane, direction, secondOffset); + // We found both so we are done. + if (sourceNeighborSearch.source && sourceNeighborSearch.neighbor) + { + return sourceNeighborSearch; + } + // We only found the focus, which means that its neighbor might be in the + // first branch. + if (sourceNeighborSearch.source) + { + // If we can possibly have both sides of a direction, check if the sibling has the neighbor + if (DirectionMatchesSplit(direction, _splitState)) + { + return _firstChild->_FindNeighborForPane(direction, sourceNeighborSearch, true, firstOffset); + } + return sourceNeighborSearch; + } + + return { nullptr, nullptr, offset }; +} + // Method Description: // - Called when our attached control is closed. Triggers listeners to our close // event, if we're a leaf pane. @@ -345,11 +758,9 @@ void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundatio return; } - const auto settings{ winrt::TerminalApp::implementation::AppLogic::CurrentAppSettings() }; - auto paneProfile = settings.FindProfile(_profile.value()); - if (paneProfile) + if (_profile) { - auto mode = paneProfile.CloseOnExit(); + const auto mode = _profile.CloseOnExit(); if ((mode == CloseOnExitMode::Always) || (mode == CloseOnExitMode::Graceful && newConnectionState == ConnectionState::Closed)) { @@ -373,27 +784,25 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect { return; } - const auto settings{ winrt::TerminalApp::implementation::AppLogic::CurrentAppSettings() }; - auto paneProfile = settings.FindProfile(_profile.value()); - if (paneProfile) + if (_profile) { // We don't want to do anything if nothing is set, so check for that first - if (static_cast(paneProfile.BellStyle()) != 0) + if (static_cast(_profile.BellStyle()) != 0) { - if (WI_IsFlagSet(paneProfile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible)) + if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible)) { // Audible is set, play the sound const auto soundAlias = reinterpret_cast(SND_ALIAS_SYSTEMHAND); PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); } - if (WI_IsFlagSet(paneProfile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window)) + if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window)) { _control.BellLightOn(); } // raise the event with the bool value corresponding to the taskbar flag - _PaneRaiseBellHandlers(nullptr, WI_IsFlagSet(paneProfile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Taskbar)); + _PaneRaiseBellHandlers(nullptr, WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Taskbar)); } } } @@ -534,18 +943,18 @@ void Pane::SetActive() } // Method Description: -// - Returns nullopt if no children of this pane were the last control to be -// focused, or the GUID of the profile of the last control to be focused (if -// there was one). +// - Returns nullptr if no children of this pane were the last control to be +// focused, or the profile of the last control to be focused (if there was +// one). // Arguments: // - // Return Value: -// - nullopt if no children of this pane were the last control to be -// focused, else the GUID of the profile of the last control to be focused -std::optional Pane::GetFocusedProfile() +// - nullptr if no children of this pane were the last control to be +// focused, else the profile of the last control to be focused +Profile Pane::GetFocusedProfile() { auto lastFocused = GetActivePane(); - return lastFocused ? lastFocused->_profile : std::nullopt; + return lastFocused ? lastFocused->_profile : nullptr; } // Method Description: @@ -640,43 +1049,111 @@ void Pane::_FocusFirstChild() } // Method Description: -// - Attempts to update the settings of this pane or any children of this pane. -// * If this pane is a leaf, and our profile guid matches the parameter, then -// we'll apply the new settings to our control. -// * If we're not a leaf, we'll recurse on our children. +// - Updates the settings of this pane, presuming that it is a leaf. // Arguments: // - settings: The new TerminalSettings to apply to any matching controls -// - profile: The GUID of the profile these settings should apply to. +// - profile: The profile from which these settings originated. // Return Value: // - -void Pane::UpdateSettings(const TerminalSettingsCreateResult& settings, const GUID& profile) +void Pane::UpdateSettings(const TerminalSettingsCreateResult& settings, const Profile& profile) { - if (!_IsLeaf()) + assert(_IsLeaf()); + + _profile = profile; + auto controlSettings = _control.Settings().as(); + // Update the parent of the control's settings object (and not the object itself) so + // that any overrides made by the control don't get affected by the reload + controlSettings.SetParent(settings.DefaultSettings()); + auto unfocusedSettings{ settings.UnfocusedSettings() }; + if (unfocusedSettings) { - _firstChild->UpdateSettings(settings, profile); - _secondChild->UpdateSettings(settings, profile); + // Note: the unfocused settings needs to be entirely unchanged _except_ we need to + // set its parent to the settings object that lives in the control. This is because + // the overrides made by the control live in that settings object, so we want to make + // sure the unfocused settings inherit from that. + unfocusedSettings.SetParent(controlSettings); } - else + _control.UnfocusedAppearance(unfocusedSettings); + _control.UpdateSettings(); +} + +// Method Description: +// - Attempts to add the provided pane as a split of the current pane. +// Arguments: +// - pane: the new pane to add +// - splitType: How the pane should be attached +// Return Value: +// - the new reference to the child created from the current pane. +std::shared_ptr Pane::AttachPane(std::shared_ptr pane, SplitState splitType) +{ + // Splice the new pane into the tree + const auto [first, _] = _Split(splitType, .5, pane); + + // If the new pane has a child that was the focus, re-focus it + // to steal focus from the currently focused pane. + if (pane->_HasFocusedChild()) { - if (profile == _profile) - { - auto controlSettings = _control.Settings().as(); - // Update the parent of the control's settings object (and not the object itself) so - // that any overrides made by the control don't get affected by the reload - controlSettings.SetParent(settings.DefaultSettings()); - auto unfocusedSettings{ settings.UnfocusedSettings() }; - if (unfocusedSettings) + pane->WalkTree([](auto p) { + if (p->_lastActive) { - // Note: the unfocused settings needs to be entirely unchanged _except_ we need to - // set its parent to the settings object that lives in the control. This is because - // the overrides made by the control live in that settings object, so we want to make - // sure the unfocused settings inherit from that. - unfocusedSettings.SetParent(controlSettings); + p->_FocusFirstChild(); + return true; } - _control.UnfocusedAppearance(unfocusedSettings); - _control.UpdateSettings(); - } + return false; + }); } + + return first; +} + +// Method Description: +// - Attempts to find the parent of the target pane, +// if found remove the pane from the tree and return it. +// - If the removed pane was (or contained the focus) the first sibling will +// gain focus. +// Arguments: +// - pane: the pane to detach +// Return Value: +// - The removed pane, if found. +std::shared_ptr Pane::DetachPane(std::shared_ptr pane) +{ + // We can't remove a pane if we only have a reference to a leaf, even if we + // are the pane. + if (_IsLeaf()) + { + return nullptr; + } + + // Check if either of our children matches the search + const auto isFirstChild = _firstChild == pane; + const auto isSecondChild = _secondChild == pane; + + if (isFirstChild || isSecondChild) + { + // Keep a reference to the child we are removing + auto detached = isFirstChild ? _firstChild : _secondChild; + // Remove the child from the tree, replace the current node with the + // other child. + _CloseChild(isFirstChild); + + detached->_borders = Borders::None; + detached->_UpdateBorders(); + + // Trigger the detached event on each child + detached->WalkTree([](auto pane) { + pane->_PaneDetachedHandlers(pane); + return false; + }); + + return detached; + } + + if (const auto detached = _firstChild->DetachPane(pane)) + { + return detached; + } + + return _secondChild->DetachPane(pane); } // Method Description: @@ -741,12 +1218,10 @@ void Pane::_CloseChild(const bool closeFirst) // them. _lastActive = _firstChild->_lastActive || _secondChild->_lastActive; - // Remove all the ui elements of our children. This'll make sure we can - // re-attach the TermControl to our Grid. - _firstChild->_root.Children().Clear(); - _secondChild->_root.Children().Clear(); - _firstChild->_border.Child(nullptr); - _secondChild->_border.Child(nullptr); + // Remove all the ui elements of the remaining child. This'll make sure + // we can re-attach the TermControl to our Grid. + remainingChild->_root.Children().Clear(); + remainingChild->_border.Child(nullptr); // Reset our UI: _root.Children().Clear(); @@ -793,17 +1268,8 @@ void Pane::_CloseChild(const bool closeFirst) } else { - // Determine which border flag we gave to the child when we first split - // it, so that we can take just that flag away from them. - Borders clearBorderFlag = Borders::None; - if (_splitState == SplitState::Horizontal) - { - clearBorderFlag = closeFirst ? Borders::Top : Borders::Bottom; - } - else if (_splitState == SplitState::Vertical) - { - clearBorderFlag = closeFirst ? Borders::Left : Borders::Right; - } + // Find what borders need to persist after we close the child + auto remainingBorders = _GetCommonBorders(); // First stash away references to the old panes and their tokens const auto oldFirstToken = _firstClosedToken; @@ -860,13 +1326,9 @@ void Pane::_CloseChild(const bool closeFirst) _root.Children().Append(_firstChild->GetRootElement()); _root.Children().Append(_secondChild->GetRootElement()); - // Take the flag away from the children that they inherited from their - // parent, and update their borders to visually match - WI_ClearAllFlags(_firstChild->_borders, clearBorderFlag); - WI_ClearAllFlags(_secondChild->_borders, clearBorderFlag); - _UpdateBorders(); - _firstChild->_UpdateBorders(); - _secondChild->_UpdateBorders(); + // Propagate the new borders down to the children. + _borders = remainingBorders; + _ApplySplitDefinitions(); // If the closed child was focused, transfer the focus to it's first sibling. if (closedChild->_lastActive) @@ -911,7 +1373,6 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) auto removedChild = closeFirst ? _firstChild : _secondChild; auto remainingChild = closeFirst ? _secondChild : _firstChild; const bool splitWidth = _splitState == SplitState::Vertical; - const auto totalSize = splitWidth ? _root.ActualWidth() : _root.ActualHeight(); Size removedOriginalSize{ ::base::saturated_cast(removedChild->_root.ActualWidth()), @@ -1118,8 +1579,26 @@ void Pane::_UpdateBorders() _border.BorderThickness(ThicknessHelper::FromLengths(left, top, right, bottom)); } +// Method Description: +// - Find the borders for the leaf pane, or the shared borders for child panes. +// Arguments: +// - +// Return Value: +// - +Borders Pane::_GetCommonBorders() +{ + if (_IsLeaf()) + { + return _borders; + } + + return _firstChild->_GetCommonBorders() & _secondChild->_GetCommonBorders(); +} + // Method Description: // - Sets the row/column of our child UI elements, to match our current split type. +// - In case the split definition or parent borders were changed, this recursively +// updates the children as well. // Arguments: // - // Return Value: @@ -1135,9 +1614,8 @@ void Pane::_ApplySplitDefinitions() _secondChild->_borders = _borders | Borders::Left; _borders = Borders::None; - _UpdateBorders(); - _firstChild->_UpdateBorders(); - _secondChild->_UpdateBorders(); + _firstChild->_ApplySplitDefinitions(); + _secondChild->_ApplySplitDefinitions(); } else if (_splitState == SplitState::Horizontal) { @@ -1148,10 +1626,10 @@ void Pane::_ApplySplitDefinitions() _secondChild->_borders = _borders | Borders::Top; _borders = Borders::None; - _UpdateBorders(); - _firstChild->_UpdateBorders(); - _secondChild->_UpdateBorders(); + _firstChild->_ApplySplitDefinitions(); + _secondChild->_ApplySplitDefinitions(); } + _UpdateBorders(); } // Method Description: @@ -1386,13 +1864,13 @@ std::optional Pane::PreCalculateCanSplit(const std::shared_ptr targe // we'll create two new children, and place them side-by-side in our Grid. // Arguments: // - splitType: what type of split we want to create. -// - profile: The profile GUID to associate with the newly created pane. +// - profile: The profile to associate with the newly created pane. // - control: A TermControl to use in the new pane. // Return Value: // - The two newly created Panes std::pair, std::shared_ptr> Pane::Split(SplitState splitType, const float splitSize, - const GUID& profile, + const Profile& profile, const TermControl& control) { if (!_IsLeaf()) @@ -1409,7 +1887,45 @@ std::pair, std::shared_ptr> Pane::Split(SplitState s return { nullptr, nullptr }; } - return _Split(splitType, splitSize, profile, control); + auto newPane = std::make_shared(profile, control); + return _Split(splitType, splitSize, newPane); +} + +// Method Description: +// - Toggle the split orientation of the currently focused pane +// Arguments: +// - +// Return Value: +// - true if a split was changed +bool Pane::ToggleSplitOrientation() +{ + // If we are a leaf there is no split to toggle. + if (_IsLeaf()) + { + return false; + } + + // Check if either our first or second child is the currently focused leaf. + // If they are then switch the split orientation on the current pane. + const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; + const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; + if (firstIsFocused || secondIsFocused) + { + // Switch the split orientation + _splitState = _splitState == SplitState::Horizontal ? SplitState::Vertical : SplitState::Horizontal; + + // then update the borders and positioning on ourselves and our children. + _borders = _GetCommonBorders(); + // Since we changed if we are using rows/columns, make sure we remove the old definitions + _root.ColumnDefinitions().Clear(); + _root.RowDefinitions().Clear(); + _CreateRowColDefinitions(); + _ApplySplitDefinitions(); + + return true; + } + + return _firstChild->ToggleSplitOrientation() || _secondChild->ToggleSplitOrientation(); } // Method Description: @@ -1442,14 +1958,13 @@ SplitState Pane::_convertAutomaticSplitState(const SplitState& splitType) const // creates a new Pane to host the control, registers event handlers. // Arguments: // - splitType: what type of split we should create. -// - profile: The profile GUID to associate with the newly created pane. -// - control: A TermControl to use in the new pane. +// - splitSize: what fraction of the pane the new pane should get +// - newPane: the pane to add as a child // Return Value: // - The two newly created Panes std::pair, std::shared_ptr> Pane::_Split(SplitState splitType, const float splitSize, - const GUID& profile, - const TermControl& control) + std::shared_ptr newPane) { if (splitType == SplitState::None) { @@ -1485,11 +2000,11 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState // Create two new Panes // Move our control, guid into the first one. // Move the new guid, control into the second. - _firstChild = std::make_shared(_profile.value(), _control); + _firstChild = std::make_shared(_profile, _control); _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected); - _profile = std::nullopt; + _profile = nullptr; _control = { nullptr }; - _secondChild = std::make_shared(profile, control); + _secondChild = newPane; _CreateRowColDefinitions(); @@ -1625,6 +2140,63 @@ bool Pane::FocusPane(const uint32_t id) } return false; } +// Method Description: +// - Focuses the given pane if it is in the tree. +// This deliberately mirrors FocusPane(id) instead of just calling +// _FocusFirstChild directly. +// Arguments: +// - the pane to focus +// Return Value: +// - true if focus was set +bool Pane::FocusPane(const std::shared_ptr pane) +{ + if (_IsLeaf() && this == pane.get()) + { + // Make sure to use _FocusFirstChild here - that'll properly update the + // focus if we're in startup. + _FocusFirstChild(); + return true; + } + else + { + if (_firstChild && _secondChild) + { + return _firstChild->FocusPane(pane) || + _secondChild->FocusPane(pane); + } + } + return false; +} + +// Method Description: +// - Recursive function that finds a pane with the given ID +// Arguments: +// - The ID of the pane we want to find +// Return Value: +// - A pointer to the pane with the given ID, if found. +std::shared_ptr Pane::FindPane(const uint32_t id) +{ + if (_IsLeaf()) + { + if (id == _id) + { + return shared_from_this(); + } + } + else + { + if (auto pane = _firstChild->FindPane(id)) + { + return pane; + } + if (auto pane = _secondChild->FindPane(id)) + { + return pane; + } + } + + return nullptr; +} // Method Description: // - Gets the size in pixels of each of our children, given the full size they @@ -2041,10 +2613,10 @@ void Pane::_SetupResources() s_focusedBorderBrush = SolidColorBrush{ Colors::Black() }; } - const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); - if (res.HasKey(tabViewBackgroundKey)) + const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); + if (res.HasKey(unfocusedBorderBrushKey)) { - winrt::Windows::Foundation::IInspectable obj = res.Lookup(tabViewBackgroundKey); + winrt::Windows::Foundation::IInspectable obj = res.Lookup(unfocusedBorderBrushKey); s_unfocusedBorderBrush = obj.try_as(); } else @@ -2133,6 +2705,30 @@ bool Pane::ContainsReadOnly() const return _IsLeaf() ? _control.ReadOnly() : (_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly()); } +// Method Description: +// - If we're a parent, place the taskbar state for all our leaves into the +// provided vector. +// - If we're a leaf, place our own state into the vector. +// Arguments: +// - states: a vector that will receive all the states of all leaves in the tree +// Return Value: +// - +void Pane::CollectTaskbarStates(std::vector& states) +{ + if (_IsLeaf()) + { + auto tbState{ winrt::make(_control.TaskbarState(), + _control.TaskbarProgress()) }; + states.push_back(tbState); + } + else + { + _firstChild->CollectTaskbarStates(states); + _secondChild->CollectTaskbarStates(states); + } +} + DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, LostFocus, _LostFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); +DEFINE_EVENT(Pane, Detached, _PaneDetachedHandlers, winrt::delegate>); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 6e7f801b7..76bab850b 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -21,6 +21,18 @@ #pragma once #include "../../cascadia/inc/cppwinrt_utils.h" +#include "TaskbarState.h" + +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class TabTests; +}; + +namespace winrt::TerminalApp::implementation +{ + struct TerminalTab; +} enum class Borders : int { @@ -35,13 +47,21 @@ DEFINE_ENUM_FLAG_OPERATORS(Borders); class Pane : public std::enable_shared_from_this { public: - Pane(const GUID& profile, + Pane(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, const winrt::Microsoft::Terminal::Control::TermControl& control, const bool lastFocused = false); std::shared_ptr GetActivePane(); winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl(); - std::optional GetFocusedProfile(); + winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile(); + + // Method Description: + // - If this is a leaf pane, return its profile. + // - If this is a branch/root pane, return nullptr. + winrt::Microsoft::Terminal::Settings::Model::Profile GetProfile() const + { + return _profile; + } winrt::Windows::UI::Xaml::Controls::Grid GetRootElement(); @@ -51,16 +71,21 @@ public: void SetActive(); void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, - const GUID& profile); + const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void Relayout(); bool ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); - bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + std::shared_ptr NavigateDirection(const std::shared_ptr sourcePane, const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool SwapPanes(std::shared_ptr first, std::shared_ptr second); + + std::shared_ptr NextPane(const std::shared_ptr pane); + std::shared_ptr PreviousPane(const std::shared_ptr pane); std::pair, std::shared_ptr> Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, - const GUID& profile, + const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, const winrt::Microsoft::Terminal::Control::TermControl& control); + bool ToggleSplitOrientation(); float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; std::optional PreCalculateAutoSplit(const std::shared_ptr target, const winrt::Windows::Foundation::Size parentSize) const; @@ -71,6 +96,10 @@ public: void Shutdown(); void Close(); + std::shared_ptr AttachPane(std::shared_ptr pane, + winrt::Microsoft::Terminal::Settings::Model::SplitState splitType); + std::shared_ptr DetachPane(std::shared_ptr pane); + int GetLeafPaneCount() const noexcept; void Maximize(std::shared_ptr zoomedPane); @@ -79,15 +108,47 @@ public: std::optional Id() noexcept; void Id(uint32_t id) noexcept; bool FocusPane(const uint32_t id); + bool FocusPane(const std::shared_ptr pane); + std::shared_ptr FindPane(const uint32_t id); bool ContainsReadOnly() const; + // Method Description: + // - A helper method for ad-hoc recursion on a pane tree. Walks the pane + // tree, calling a predicate on each pane in a depth-first pattern. + // - If the predicate returns true, recursion is stopped early. + // Arguments: + // - f: The function to be applied to each pane. + // Return Value: + // - true if the predicate returned true on any pane. + template + //requires std::predicate> + bool WalkTree(F f) + { + if (f(shared_from_this())) + { + return true; + } + + if (!_IsLeaf()) + { + return _firstChild->WalkTree(f) || _secondChild->WalkTree(f); + } + + return false; + } + + void CollectTaskbarStates(std::vector& states); + WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate>); DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); + DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate>); private: + struct PanePoint; + struct PaneNeighborSearch; struct SnapSizeResult; struct SnapChildrenSizeResult; struct LayoutSizeNode; @@ -107,7 +168,7 @@ private: std::optional _id; bool _lastActive{ false }; - std::optional _profile{ std::nullopt }; + winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr }; winrt::event_token _connectionStateChangedToken{ 0 }; winrt::event_token _firstClosedToken{ 0 }; winrt::event_token _secondClosedToken{ 0 }; @@ -128,16 +189,25 @@ private: std::pair, std::shared_ptr> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, - const GUID& profile, - const winrt::Microsoft::Terminal::Control::TermControl& control); + std::shared_ptr newPane); void _CreateRowColDefinitions(); void _ApplySplitDefinitions(); void _SetupEntranceAnimation(); void _UpdateBorders(); + Borders _GetCommonBorders(); bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); - bool _NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + + std::shared_ptr _FindParentOfPane(const std::shared_ptr pane); + bool _IsAdjacent(const std::shared_ptr first, const PanePoint firstOffset, const std::shared_ptr second, const PanePoint secondOffset, const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction) const; + PaneNeighborSearch _FindNeighborForPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction, + PaneNeighborSearch searchResult, + const bool focusIsSecondSide, + const PanePoint offset); + PaneNeighborSearch _FindPaneAndNeighbor(const std::shared_ptr sourcePane, + const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction, + const PanePoint offset); void _CloseChild(const bool closeFirst); winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst); @@ -200,6 +270,19 @@ private: static void _SetupResources(); + struct PanePoint + { + float x; + float y; + }; + + struct PaneNeighborSearch + { + std::shared_ptr source; + std::shared_ptr neighbor; + PanePoint sourceOffset; + }; + struct SnapSizeResult { float lower; @@ -236,4 +319,7 @@ private: private: void _AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode); }; + + friend struct winrt::TerminalApp::implementation::TerminalTab; + friend class ::TerminalAppLocalTests::TabTests; }; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 9537bf48c..f9e8cbf12 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -282,6 +282,16 @@ Move focus the tab at the given index + + Move focused pane to the tab at the given index + + + Move focused pane to another tab + + + An alias for the "move-pane" subcommand. + {Locked="\"move-pane\""} + Specify the size as a percentage of the parent pane. Valid values are between (0,1), exclusive. @@ -359,6 +369,12 @@ The direction to move focus in + + Swap the focused pane with the adjacent pane in the specified direction + + + The direction to move the focused pane in + Launch the window in focus mode @@ -637,6 +653,14 @@ Command Palette + + Focus Terminal + This is displayed as a label for the context menu item that focuses the terminal. + + + Windows + This is displayed as a label for the context menu item that holds the submenu of available windows. + Open in Windows Terminal (Dev) {Locked} The dev build will never be seen in multiple languages @@ -649,4 +673,16 @@ Open in Windows Terminal {Locked="Windows"} This is a menu item that will be displayed in the Windows File Explorer that launches the non-preview version of Windows Terminal + + Open a new tab in given starting directory + + + Split Tab + + + Open a new window with given starting directory + + + Split the window and start in given directory + diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 718d8d6e2..16149d550 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -56,13 +56,13 @@ namespace winrt::TerminalApp::implementation // - existingConnection: An optional connection that is already established to a PTY // for this tab to host instead of creating one. // If not defined, the tab will create the connection. - void TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection) + HRESULT TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection) try { - const auto profileGuid{ _settings.GetProfileForArgs(newTerminalArgs) }; + const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) }; const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) }; - _CreateNewTabFromSettings(profileGuid, settings, existingConnection); + _CreateNewTabWithProfileAndSettings(profile, settings, existingConnection); const uint32_t tabCount = _tabs.Size(); const bool usedManualProfile = (newTerminalArgs != nullptr) && @@ -70,7 +70,7 @@ namespace winrt::TerminalApp::implementation newTerminalArgs.Profile().empty()); // Lookup the name of the color scheme used by this profile. - const auto scheme = _settings.GetColorSchemeForProfile(profileGuid); + const auto scheme = _settings.GetColorSchemeForProfile(profile); // If they explicitly specified `null` as the scheme (indicating _no_ scheme), log // that as the empty string. const auto schemeName = scheme ? scheme.Name() : L"\0"; @@ -82,48 +82,25 @@ namespace winrt::TerminalApp::implementation TraceLoggingUInt32(1u, "EventVer", "Version of this event"), TraceLoggingUInt32(tabCount, "TabCount", "Count of tabs currently opened in TerminalApp"), TraceLoggingBool(usedManualProfile, "ProfileSpecified", "Whether the new tab specified a profile explicitly"), - TraceLoggingGuid(profileGuid, "ProfileGuid", "The GUID of the profile spawned in the new tab"), + TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The GUID of the profile spawned in the new tab"), TraceLoggingBool(settings.DefaultSettings().UseAcrylic(), "UseAcrylic", "The acrylic preference from the settings"), TraceLoggingFloat64(settings.DefaultSettings().TintOpacity(), "TintOpacity", "Opacity preference from the settings"), TraceLoggingWideString(settings.DefaultSettings().FontFace().c_str(), "FontFace", "Font face chosen in the settings"), TraceLoggingWideString(schemeName.data(), "SchemeName", "Color scheme set in the settings"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + + return S_OK; } - CATCH_LOG(); + CATCH_RETURN(); // Method Description: - // - Creates a new tab with the given settings. If the tab bar is not being - // currently displayed, it will be shown. + // - Sets up state, event handlers, etc on a tab object that was just made. // Arguments: - // - profileGuid: ID to use to lookup profile settings for this connection - // - settings: the TerminalSettings object to use to create the TerminalControl with. - // - existingConnection: optionally receives a connection from the outside world instead of attempting to create one - void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection) + // - newTabImpl: the uninitialized tab. + void TerminalPage::_InitializeTab(winrt::com_ptr newTabImpl) { - // Initialize the new tab - // Create a connection based on the values in our settings object if we weren't given one. - auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profileGuid, settings.DefaultSettings()); - - TerminalConnection::ITerminalConnection debugConnection{ nullptr }; - if (_settings.GlobalSettings().DebugFeaturesEnabled()) - { - const CoreWindow window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - if (bothAltsPressed) - { - std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); - } - } - - // Give term control a child of the settings so that any overrides go in the child - // This way, when we do a settings reload we just update the parent and the overrides remain - auto term = _InitControl(settings, connection); - - auto newTabImpl = winrt::make_self(profileGuid, term); + newTabImpl->Initialize(); // Add the new tab to the list of our tabs. _tabs.Append(*newTabImpl); @@ -136,7 +113,7 @@ namespace winrt::TerminalApp::implementation _UpdateTabIndices(); // Hookup our event handlers to the new terminal - _RegisterTerminalEvents(term, *newTabImpl); + _RegisterTabEvents(*newTabImpl); // Don't capture a strong ref to the tab. If the tab is removed as this // is called, we don't really care anymore about handling the event. @@ -184,14 +161,26 @@ namespace winrt::TerminalApp::implementation } }); + newTabImpl->SplitTabRequested([weakTab, weakThis{ get_weak() }]() { + auto page{ weakThis.get() }; + auto tab{ weakTab.get() }; + + if (page && tab) + { + page->_SplitTab(*tab); + } + }); + auto tabViewItem = newTabImpl->TabViewItem(); _tabView.TabItems().Append(tabViewItem); // Set this tab's icon to the icon from the user's profile - const auto profile = _settings.FindProfile(profileGuid); - if (profile != nullptr && !profile.Icon().empty()) + if (const auto profile{ newTabImpl->GetFocusedProfile() }) { - newTabImpl->UpdateIcon(profile.Icon()); + if (!profile.Icon().empty()) + { + newTabImpl->UpdateIcon(profile.Icon()); + } } tabViewItem.PointerReleased({ this, &TerminalPage::_OnTabClick }); @@ -224,19 +213,73 @@ namespace winrt::TerminalApp::implementation } }); - if (debugConnection) // this will only be set if global debugging is on and tap is active - { - auto newControl = _InitControl(settings, debugConnection); - _RegisterTerminalEvents(newControl, *newTabImpl); - // Split (auto) with the debug tap. - newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profileGuid, newControl); - } - // This kicks off TabView::SelectionChanged, in response to which // we'll attach the terminal's Xaml control to the Xaml root. _tabView.SelectedItem(tabViewItem); } + // Method Description: + // - Create a new tab using a specified pane as the root. + // Arguments: + // - pane: The pane to use as the root. + void TerminalPage::_CreateNewTabFromPane(std::shared_ptr pane) + { + auto newTabImpl = winrt::make_self(pane); + _InitializeTab(newTabImpl); + } + + // Method Description: + // - Creates a new tab with the given settings. If the tab bar is not being + // currently displayed, it will be shown. + // Arguments: + // - profile: profile settings for this connection + // - settings: the TerminalSettings object to use to create the TerminalControl with. + // - existingConnection: optionally receives a connection from the outside world instead of attempting to create one + void TerminalPage::_CreateNewTabWithProfileAndSettings(const Profile& profile, const TerminalSettingsCreateResult& settings, TerminalConnection::ITerminalConnection existingConnection) + { + // Initialize the new tab + // Create a connection based on the values in our settings object if we weren't given one. + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, settings.DefaultSettings()); + + // If we had an `existingConnection`, then this is an inbound handoff from somewhere else. + // We need to tell it about our size information so it can match the dimensions of what + // we are about to present. + if (existingConnection) + { + connection.Resize(settings.DefaultSettings().InitialRows(), settings.DefaultSettings().InitialCols()); + } + + TerminalConnection::ITerminalConnection debugConnection{ nullptr }; + if (_settings.GlobalSettings().DebugFeaturesEnabled()) + { + const CoreWindow window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + if (bothAltsPressed) + { + std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); + } + } + + // Give term control a child of the settings so that any overrides go in the child + // This way, when we do a settings reload we just update the parent and the overrides remain + auto term = _InitControl(settings, connection); + + auto newTabImpl = winrt::make_self(profile, term); + _RegisterTerminalEvents(term); + _InitializeTab(newTabImpl); + + if (debugConnection) // this will only be set if global debugging is on and tap is active + { + auto newControl = _InitControl(settings, debugConnection); + _RegisterTerminalEvents(newControl); + // Split (auto) with the debug tap. + newTabImpl->SplitPane(SplitState::Automatic, 0.5f, profile, newControl); + } + } + // Method Description: // - Get the icon of the currently focused terminal control, and set its // tab's icon to that icon. @@ -244,19 +287,9 @@ namespace winrt::TerminalApp::implementation // - tab: the Tab to update the title for. void TerminalPage::_UpdateTabIcon(TerminalTab& tab) { - const auto lastFocusedProfileOpt = tab.GetFocusedProfile(); - if (lastFocusedProfileOpt.has_value()) + if (const auto profile = tab.GetFocusedProfile()) { - const auto lastFocusedProfile = lastFocusedProfileOpt.value(); - const auto matchingProfile = _settings.FindProfile(lastFocusedProfile); - if (matchingProfile) - { - tab.UpdateIcon(matchingProfile.Icon()); - } - else - { - tab.UpdateIcon({}); - } + tab.UpdateIcon(profile.Icon()); } } @@ -310,23 +343,16 @@ namespace winrt::TerminalApp::implementation { try { - // TODO: GH#5047 - In the future, we should get the Profile of - // the focused pane, and use that to build a new instance of the - // settings so we can duplicate this tab/pane. + // TODO: GH#5047 - We're duplicating the whole profile, which might + // be a dangling reference to old settings. // - // Currently, if the profile doesn't exist anymore in our - // settings, we'll silently do nothing. - // - // In the future, it will be preferable to just duplicate the - // current control's settings, but we can't do that currently, - // because we won't be able to create a new instance of the - // connection without keeping an instance of the original Profile - // object around. + // In the future, it may be preferable to just duplicate the + // current control's live settings (which will include changes + // made through VT). - const auto& profileGuid = tab.GetFocusedProfile(); - if (profileGuid.has_value()) + if (const auto profile = tab.GetFocusedProfile()) { - const auto settingsCreateResult{ TerminalSettings::CreateWithProfileByID(_settings, profileGuid.value(), *_bindings) }; + const auto settingsCreateResult{ TerminalSettings::CreateWithProfile(_settings, profile, *_bindings) }; const auto workingDirectory = tab.GetActiveTerminalControl().WorkingDirectory(); const auto validWorkingDirectory = !workingDirectory.empty(); if (validWorkingDirectory) @@ -334,7 +360,7 @@ namespace winrt::TerminalApp::implementation settingsCreateResult.DefaultSettings().StartingDirectory(workingDirectory); } - _CreateNewTabFromSettings(profileGuid.value(), settingsCreateResult); + _CreateNewTabWithProfileAndSettings(profile, settingsCreateResult); const auto runtimeTabText{ tab.GetTabText() }; if (!runtimeTabText.empty()) @@ -349,6 +375,20 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); } + // Method Description: + // - Sets the specified tab as the focused tab and splits its active pane + // Arguments: + // - tab: tab to split + void TerminalPage::_SplitTab(TerminalTab& tab) + { + try + { + _SetFocusedTab(tab); + _SplitPane(tab, SplitState::Automatic, SplitType::Duplicate); + } + CATCH_LOG(); + } + // Method Description: // - Removes the tab (both TerminalControl and XAML) after prompting for approval // Arguments: @@ -497,24 +537,25 @@ namespace winrt::TerminalApp::implementation // TerminalPage::_OnTabSelectionChanged // Return Value: // true iff we were able to select that tab index, false otherwise - bool TerminalPage::_SelectTab(const uint32_t tabIndex) + bool TerminalPage::_SelectTab(uint32_t tabIndex) { - if (tabIndex >= 0 && tabIndex < _tabs.Size()) - { - auto tab{ _tabs.GetAt(tabIndex) }; - if (_startupState == StartupState::InStartup) - { - _tabView.SelectedItem(tab.TabViewItem()); - _UpdatedSelectedTab(tab); - } - else - { - _SetFocusedTab(tab); - } + // GH#9369 - if the argument is out of range, then clamp to the number + // of available tabs. Previously, we'd just silently do nothing if the + // value was greater than the number of tabs. + tabIndex = std::clamp(tabIndex, 0u, _tabs.Size() - 1); - return true; + auto tab{ _tabs.GetAt(tabIndex) }; + if (_startupState == StartupState::InStartup) + { + _tabView.SelectedItem(tab.TabViewItem()); + _UpdatedSelectedTab(tab); } - return false; + else + { + _SetFocusedTab(tab); + } + + return true; } // Method Description: diff --git a/src/cascadia/TerminalApp/TabRowControl.cpp b/src/cascadia/TerminalApp/TabRowControl.cpp index bc4d7a839..fa9a5ff98 100644 --- a/src/cascadia/TerminalApp/TabRowControl.cpp +++ b/src/cascadia/TerminalApp/TabRowControl.cpp @@ -5,9 +5,19 @@ #include "TabRowControl.h" #include "TabRowControl.g.cpp" +#include + +using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt; using namespace winrt::Microsoft::UI::Xaml; +using namespace winrt::Windows::UI::Text; + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; +} namespace winrt::TerminalApp::implementation { @@ -23,4 +33,53 @@ namespace winrt::TerminalApp::implementation void TabRowControl::OnNewTabButtonClick(IInspectable const&, Controls::SplitButtonClickEventArgs const&) { } + + // Method Description: + // - Bound in Drag&Drop of the Xaml editor to the [+] button. + // Arguments: + // + void TabRowControl::OnNewTabButtonDrop(IInspectable const&, winrt::Windows::UI::Xaml::DragEventArgs const&) + { + } + + // Method Description: + // - Bound in Drag-over of the Xaml editor to the [+] button. + // Allows drop of 'StorageItems' which will be used as StartingDirectory + // Arguments: + // - + // - e: DragEventArgs which hold the items + void TabRowControl::OnNewTabButtonDragOver(IInspectable const&, winrt::Windows::UI::Xaml::DragEventArgs const& e) + { + // We can only handle drag/dropping StorageItems (files). + // If the format on the clipboard is anything else, returning + // early here will prevent the drag/drop from doing anything. + if (!e.DataView().Contains(StandardDataFormats::StorageItems())) + { + return; + } + + // Make sure to set the AcceptedOperation, so that we can later receive the path in the Drop event + e.AcceptedOperation(DataPackageOperation::Copy); + + const auto modifiers = static_cast(e.Modifiers()); + if (WI_IsFlagSet(modifiers, static_cast(DragDrop::DragDropModifiers::Alt))) + { + e.DragUIOverride().Caption(RS_(L"DropPathTabSplit/Text")); + } + else if (WI_IsFlagSet(modifiers, static_cast(DragDrop::DragDropModifiers::Shift))) + { + e.DragUIOverride().Caption(RS_(L"DropPathTabNewWindow/Text")); + } + else + { + e.DragUIOverride().Caption(RS_(L"DropPathTabRun/Text")); + } + + // Sets if the caption is visible + e.DragUIOverride().IsCaptionVisible(true); + // Sets if the dragged content is visible + e.DragUIOverride().IsContentVisible(false); + // Sets if the glyph is visible + e.DragUIOverride().IsGlyphVisible(false); + } } diff --git a/src/cascadia/TerminalApp/TabRowControl.h b/src/cascadia/TerminalApp/TabRowControl.h index 1dedf9edb..6ffa60532 100644 --- a/src/cascadia/TerminalApp/TabRowControl.h +++ b/src/cascadia/TerminalApp/TabRowControl.h @@ -14,6 +14,8 @@ namespace winrt::TerminalApp::implementation TabRowControl(); void OnNewTabButtonClick(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const& args); + void OnNewTabButtonDrop(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e); + void OnNewTabButtonDragOver(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e); }; } diff --git a/src/cascadia/TerminalApp/TabRowControl.xaml b/src/cascadia/TerminalApp/TabRowControl.xaml index e82678bac..802e7c529 100644 --- a/src/cascadia/TerminalApp/TabRowControl.xaml +++ b/src/cascadia/TerminalApp/TabRowControl.xaml @@ -25,11 +25,14 @@ x:Uid="NewTabSplitButton" HorizontalAlignment="Left" VerticalAlignment="Stretch" + AllowDrop="True" AutomationProperties.AccessibilityView="Control" BorderThickness="0" Click="OnNewTabButtonClick" Content="" CornerRadius="{Binding Source={ThemeResource OverlayCornerRadius}, Converter={StaticResource TopCornerRadiusFilterConverter}}" + DragOver="OnNewTabButtonDragOver" + Drop="OnNewTabButtonDrop" FontFamily="Segoe MDL2 Assets" FontSize="12" FontWeight="SemiLight" diff --git a/src/cascadia/TerminalApp/TaskbarState.cpp b/src/cascadia/TerminalApp/TaskbarState.cpp new file mode 100644 index 000000000..183460556 --- /dev/null +++ b/src/cascadia/TerminalApp/TaskbarState.cpp @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TaskbarState.h" +#include "TaskbarState.g.cpp" + +namespace winrt::TerminalApp::implementation +{ + // Default to unset, 0%. + TaskbarState::TaskbarState() : + TaskbarState(0, 0){}; + + TaskbarState::TaskbarState(const uint64_t dispatchTypesState, const uint64_t progressParam) : + _State{ dispatchTypesState }, + _Progress{ progressParam } {} + + uint64_t TaskbarState::Priority() const + { + // This seemingly nonsensical ordering is from + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate#how-the-taskbar-button-chooses-the-progress-indicator-for-a-group + switch (_State) + { + case 0: // Clear = 0, + return 5; + case 1: // Set = 1, + return 3; + case 2: // Error = 2, + return 1; + case 3: // Indeterminate = 3, + return 4; + case 4: // Paused = 4 + return 2; + } + // Here, return 6, to definitely be greater than all the other valid values. + // This should never really happen. + return 6; + } + + int TaskbarState::ComparePriority(const winrt::TerminalApp::TaskbarState& lhs, const winrt::TerminalApp::TaskbarState& rhs) + { + return lhs.Priority() < rhs.Priority(); + } + +} diff --git a/src/cascadia/TerminalApp/TaskbarState.h b/src/cascadia/TerminalApp/TaskbarState.h new file mode 100644 index 000000000..e36b4440c --- /dev/null +++ b/src/cascadia/TerminalApp/TaskbarState.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +#include "inc/cppwinrt_utils.h" +#include "TaskbarState.g.h" + +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class TabTests; +}; + +namespace winrt::TerminalApp::implementation +{ + struct TaskbarState : TaskbarStateT + { + public: + TaskbarState(); + TaskbarState(const uint64_t dispatchTypesState, const uint64_t progress); + + static int ComparePriority(const winrt::TerminalApp::TaskbarState& lhs, const winrt::TerminalApp::TaskbarState& rhs); + + uint64_t Priority() const; + + WINRT_PROPERTY(uint64_t, State, 0); + WINRT_PROPERTY(uint64_t, Progress, 0); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(TaskbarState); +} diff --git a/src/cascadia/TerminalApp/TaskbarState.idl b/src/cascadia/TerminalApp/TaskbarState.idl new file mode 100644 index 000000000..159242a17 --- /dev/null +++ b/src/cascadia/TerminalApp/TaskbarState.idl @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace TerminalApp +{ + [default_interface] runtimeclass TaskbarState + { + TaskbarState(); + TaskbarState(UInt64 dispatchTypesState, UInt64 progress); + + UInt64 State{ get; }; + UInt64 Progress{ get; }; + UInt64 Priority { get; }; + } +} diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 67317f4e4..81cd56293 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -53,7 +53,7 @@ Designer - + Designer @@ -74,6 +74,7 @@ + MinMaxCloseControl.xaml @@ -89,6 +90,9 @@ TabBase.idl + + TaskbarState.idl + TerminalTab.idl @@ -112,7 +116,7 @@ HighlightedTextControl.xaml - + ColorPickupFlyout.xaml @@ -139,7 +143,7 @@ AppLogic.idl - + @@ -149,6 +153,7 @@ + MinMaxCloseControl.xaml @@ -164,6 +169,9 @@ TabBase.idl + + TaskbarState.idl + TerminalTab.idl @@ -195,7 +203,7 @@ HighlightedTextControl.xaml - + ColorPickupFlyout.xaml @@ -256,6 +264,7 @@ + TerminalPage.xaml @@ -280,7 +289,7 @@ HighlightedTextControl.xaml Code - + ColorPickupFlyout.xaml Code @@ -371,13 +380,13 @@ - + 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/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters index 1511e14d9..f041b68d7 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters @@ -13,9 +13,6 @@ pane - - tab - pane @@ -23,7 +20,6 @@ - tab @@ -49,10 +45,10 @@ highlightedText + + - - app @@ -60,9 +56,6 @@ pane - - tab - @@ -92,14 +85,13 @@ highlightedText + + app - - settings - settings @@ -107,9 +99,6 @@ settings - - tab - tab @@ -125,6 +114,9 @@ tab + + + @@ -160,9 +152,6 @@ commandPalette - - commandPalette - commandPalette diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index a5eb4c5dd..cba5ea4a9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -6,6 +6,7 @@ #include "Utils.h" #include "../../types/inc/utils.hpp" +#include #include #include "TerminalPage.g.cpp" @@ -18,6 +19,8 @@ #include "RenameWindowRequestedArgs.g.cpp" #include "../inc/WindowingBehavior.h" +#include + using namespace winrt; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::UI::Xaml; @@ -139,6 +142,44 @@ namespace winrt::TerminalApp::implementation } CATCH_LOG(); + if (_settings.GlobalSettings().UseAcrylicInTabRow()) + { + const auto res = Application::Current().Resources(); + + const auto lightKey = winrt::box_value(L"Light"); + const auto darkKey = winrt::box_value(L"Dark"); + const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); + + for (auto const& dictionary : res.MergedDictionaries()) + { + // Don't change MUX resources + if (dictionary.Source()) + { + continue; + } + + for (auto const& kvPair : dictionary.ThemeDictionaries()) + { + const auto themeDictionary = kvPair.Value().as(); + + if (themeDictionary.HasKey(tabViewBackgroundKey)) + { + const auto backgroundSolidBrush = themeDictionary.Lookup(tabViewBackgroundKey).as(); + + const til::color backgroundColor = backgroundSolidBrush.Color(); + + const auto acrylicBrush = Media::AcrylicBrush(); + acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + acrylicBrush.FallbackColor(backgroundColor); + acrylicBrush.TintColor(backgroundColor); + acrylicBrush.TintOpacity(0.5); + + themeDictionary.Insert(tabViewBackgroundKey, acrylicBrush); + } + } + } + } + _tabRow.PointerMoved({ get_weak(), &TerminalPage::_RestorePointerCursorHandler }); _tabView.CanReorderTabs(!isElevated); _tabView.CanDragTabs(!isElevated); @@ -172,40 +213,13 @@ namespace winrt::TerminalApp::implementation _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { if (auto page{ weakThis.get() }) { - // if alt is pressed, open a pane - const CoreWindow window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; - const auto rShiftState = window.GetKeyState(VirtualKey::RightShift); - const auto lShiftState = window.GetKeyState(VirtualKey::LeftShift); - const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; - - // Check for DebugTap - bool debugTap = page->_settings.GlobalSettings().DebugFeaturesEnabled() && - WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - if (altPressed && !debugTap) - { - page->_SplitPane(SplitState::Automatic, - SplitType::Manual, - 0.5f, - nullptr); - } - else if (shiftPressed && !debugTap) - { - page->_OpenNewWindow(false, NewTerminalArgs()); - } - else - { - page->_OpenNewTab(nullptr); - } + page->_OpenNewTerminal(NewTerminalArgs()); + } + }); + _newTabButton.Drop([weakThis{ get_weak() }](Windows::Foundation::IInspectable const&, winrt::Windows::UI::Xaml::DragEventArgs e) { + if (auto page{ weakThis.get() }) + { + page->NewTerminalByDrop(e); } }); _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); @@ -262,6 +276,36 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); } + winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e) + { + Windows::Foundation::Collections::IVectorView items; + try + { + items = co_await e.DataView().GetStorageItemsAsync(); + } + CATCH_LOG(); + + if (items.Size() == 1) + { + std::filesystem::path path(items.GetAt(0).Path().c_str()); + if (!std::filesystem::is_directory(path)) + { + path = path.parent_path(); + } + + NewTerminalArgs args; + args.StartingDirectory(winrt::hstring{ path.wstring() }); + this->_OpenNewTerminal(args); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabByDragDrop", + TraceLoggingDescription("Event emitted when the user drag&drops onto the new tab button"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + } + } + // Method Description: // - This method is called once command palette action was chosen for dispatching // We'll use this event to dispatch this command. @@ -344,34 +388,12 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::StartInboundListener(); } // If we failed to start the listener, it will throw. - // We should fail fast here or the Terminal will be in a very strange state. - // We only start the listener if the Terminal was started with the COM server - // `-Embedding` flag and we make no tabs as a result. - // Therefore, if the listener cannot start itself up to make that tab with - // the inbound connection that caused the COM activation in the first place... - // we would be left with an empty terminal frame with no tabs. - // Instead, crash out so COM sees the server die and things unwind - // without a weird empty frame window. + // We don't want to fail fast here because if a peasant has some trouble with + // starting the listener, we don't want it to crash and take all its tabs down + // with it. catch (...) { - // However, we cannot always fail fast because of MSFT:33501832. Sometimes the COM catalog - // tears the state between old and new versions and fails here for that reason. - // As we're always becoming an inbound server in the monarch, even when COM didn't strictly - // ask us yet...we might just crash always. - // Instead... we're going to differentiate. If COM started us... we will fail fast - // so it sees the process die and falls back. - // If we were just starting normally as a Monarch and opportunistically listening for - // inbound connections... then we'll just log the failure and move on assuming - // the version state is torn and will fix itself whenever the packaging upgrade - // tasks decide to clean up. - if (_isEmbeddingInboundListener) - { - FAIL_FAST_CAUGHT_EXCEPTION(); - } - else - { - LOG_CAUGHT_EXCEPTION(); - } + LOG_CAUGHT_EXCEPTION(); } } } @@ -435,6 +457,11 @@ namespace winrt::TerminalApp::implementation co_return; } } + + // GH#6586: now that we're done processing all startup commands, + // focus the active control. This will work as expected for both + // commandline invocations and for `wt` action invocations. + _GetActiveControl().Focus(FocusState::Programmatic); } if (initial) { @@ -622,43 +649,7 @@ namespace winrt::TerminalApp::implementation if (auto page{ weakThis.get() }) { NewTerminalArgs newTerminalArgs{ profileIndex }; - - // if alt is pressed, open a pane - const CoreWindow window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; - const auto rShiftState = window.GetKeyState(VirtualKey::RightShift); - const auto lShiftState = window.GetKeyState(VirtualKey::LeftShift); - const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; - - // Check for DebugTap - bool debugTap = page->_settings.GlobalSettings().DebugFeaturesEnabled() && - WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - if (altPressed && !debugTap) - { - page->_SplitPane(SplitState::Automatic, - SplitType::Manual, - 0.5f, - newTerminalArgs); - } - else if (shiftPressed && !debugTap) - { - // Manually fill in the evaluated profile. - newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(page->_settings.GetProfileForArgs(newTerminalArgs))); - page->_OpenNewWindow(false, newTerminalArgs); - } - else - { - page->_OpenNewTab(newTerminalArgs); - } + page->_OpenNewTerminal(newTerminalArgs); } }); newTabFlyout.Items().Append(profileMenuItem); @@ -752,6 +743,54 @@ namespace winrt::TerminalApp::implementation _newTabButton.Flyout().ShowAt(_newTabButton); } + void TerminalPage::_OpenNewTerminal(const NewTerminalArgs newTerminalArgs) + { + // if alt is pressed, open a pane + const CoreWindow window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; + const auto rShiftState = window.GetKeyState(VirtualKey::RightShift); + const auto lShiftState = window.GetKeyState(VirtualKey::LeftShift); + const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; + + // Check for DebugTap + bool debugTap = this->_settings.GlobalSettings().DebugFeaturesEnabled() && + WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + if (altPressed && !debugTap) + { + this->_SplitPane(SplitState::Automatic, + SplitType::Manual, + 0.5f, + newTerminalArgs); + } + else if (shiftPressed && !debugTap) + { + // Manually fill in the evaluated profile. + if (newTerminalArgs.ProfileIndex() != nullptr) + { + // We want to promote the index to a GUID because there is no "launch to profile index" command. + const auto profile = _settings.GetProfileForArgs(newTerminalArgs); + if (profile) + { + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); + } + } + this->_OpenNewWindow(false, newTerminalArgs); + } + else + { + LOG_IF_FAILED(this->_OpenNewTab(newTerminalArgs)); + } + } + winrt::fire_and_forget TerminalPage::_RemoveOnCloseRoutine(Microsoft::UI::Xaml::Controls::TabViewItem tabViewItem, winrt::com_ptr page) { co_await winrt::resume_foreground(page->_tabView.Dispatcher()); @@ -765,14 +804,18 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Creates a new connection based on the profile settings // Arguments: - // - the profile GUID we want the settings from + // - the profile we want the settings from // - the terminal settings // Return value: // - the desired connection - TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(GUID profileGuid, + TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(Profile profile, TerminalSettings settings) { - const auto profile = _settings.FindProfile(profileGuid); + if (!profile) + { + // Use the default profile if we didn't get one as an argument. + profile = _settings.FindProfile(_settings.GlobalSettings().DefaultProfile()); + } TerminalConnection::ITerminalConnection connection{ nullptr }; @@ -785,18 +828,20 @@ namespace winrt::TerminalApp::implementation // TODO GH#4661: Replace this with directly using the AzCon when our VT is better std::filesystem::path azBridgePath{ wil::GetModuleFileNameW(nullptr) }; azBridgePath.replace_filename(L"TerminalAzBridge.exe"); - connection = TerminalConnection::ConptyConnection(azBridgePath.wstring(), - L".", - L"Azure", - nullptr, - settings.InitialRows(), - settings.InitialCols(), - winrt::guid()); + connection = TerminalConnection::ConptyConnection(); + connection.Initialize(TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.wstring(), + L".", + L"Azure", + nullptr, + ::base::saturated_cast(settings.InitialRows()), + ::base::saturated_cast(settings.InitialCols()), + winrt::guid())); } else { - std::wstring guidWString = Utils::GuidToString(profileGuid); + // profile is guaranteed to exist here + std::wstring guidWString = Utils::GuidToString(profile.Guid()); StringMap envMap{}; envMap.Insert(L"WT_PROFILE_ID", guidWString); @@ -817,18 +862,24 @@ namespace winrt::TerminalApp::implementation // construction, because the connection might not spawn the child // process until later, on another thread, after we've already // restored the CWD to it's original value. - std::wstring cwdString{ wil::GetCurrentDirectoryW() }; - std::filesystem::path cwd{ cwdString }; - cwd /= settings.StartingDirectory().c_str(); + winrt::hstring newWorkingDirectory{ settings.StartingDirectory() }; + if (newWorkingDirectory.size() <= 1 || + !(newWorkingDirectory[0] == L'~' || newWorkingDirectory[0] == L'/')) + { // We only want to resolve the new WD against the CWD if it doesn't look like a Linux path (see GH#592) + std::wstring cwdString{ wil::GetCurrentDirectoryW() }; + std::filesystem::path cwd{ cwdString }; + cwd /= settings.StartingDirectory().c_str(); + newWorkingDirectory = winrt::hstring{ cwd.wstring() }; + } - auto conhostConn = TerminalConnection::ConptyConnection( - settings.Commandline(), - winrt::hstring{ cwd.c_str() }, - settings.StartingTitle(), - envMap.GetView(), - settings.InitialRows(), - settings.InitialCols(), - winrt::guid()); + auto conhostConn = TerminalConnection::ConptyConnection(); + conhostConn.Initialize(TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), + newWorkingDirectory, + settings.StartingTitle(), + envMap.GetView(), + ::base::saturated_cast(settings.InitialRows()), + ::base::saturated_cast(settings.InitialCols()), + winrt::guid())); sessionGuid = conhostConn.Guid(); connection = conhostConn; @@ -839,7 +890,7 @@ namespace winrt::TerminalApp::implementation "ConnectionCreated", TraceLoggingDescription("Event emitted upon the creation of a connection"), TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"), - TraceLoggingGuid(profileGuid, "ProfileGuid", "The profile's GUID"), + TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"), TraceLoggingGuid(sessionGuid, "SessionGuid", "The WT_SESSION's GUID"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); @@ -914,12 +965,14 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_KeyDownHandler(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) { - auto key = e.OriginalKey(); - auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); - auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); - auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + const auto key = e.OriginalKey(); + const auto scanCode = e.KeyStatus().ScanCode; + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); - winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, static_cast(key) }; + winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast(key), static_cast(scanCode) }; if (const auto cmd{ _settings.ActionMap().GetActionByKeyChord(kc) }) { if (CommandPalette().Visibility() == Visibility::Visible && cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) @@ -980,11 +1033,9 @@ namespace winrt::TerminalApp::implementation // handle. This includes: // * the Copy and Paste events, for setting and retrieving clipboard data // on the right thread - // * the TitleChanged event, for changing the text of the tab // Arguments: // - term: The newly created TermControl to connect the events for - // - hostingTab: The Tab that's hosting this TermControl instance - void TerminalPage::_RegisterTerminalEvents(TermControl term, TerminalTab& hostingTab) + void TerminalPage::_RegisterTerminalEvents(TermControl term) { term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); @@ -999,10 +1050,20 @@ namespace winrt::TerminalApp::implementation term.HidePointerCursor({ get_weak(), &TerminalPage::_HidePointerCursorHandler }); term.RestorePointerCursor({ get_weak(), &TerminalPage::_RestorePointerCursorHandler }); + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + } - // Bind Tab events to the TermControl and the Tab's Pane - hostingTab.Initialize(term); - + // Method Description: + // - Connects event handlers to the TerminalTab for events that we want to + // handle. This includes: + // * the TitleChanged event, for changing the text of the tab + // * the Color{Selected,Cleared} events to change the color of a tab. + // Arguments: + // - hostingTab: The Tab that's hosting this TermControl instance + void TerminalPage::_RegisterTabEvents(TerminalTab& hostingTab) + { auto weakTab{ hostingTab.get_weak() }; auto weakThis{ get_weak() }; // PropertyChanged is the generic mechanism by which the Tab @@ -1054,7 +1115,6 @@ namespace winrt::TerminalApp::implementation // Add an event handler for when the terminal or tab wants to set a // progress indicator on the taskbar hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); // TODO GH#3327: Once we support colorizing the NewTab button based on // the color of the tab, we'll want to make sure to call @@ -1101,14 +1161,33 @@ namespace winrt::TerminalApp::implementation // Arguments: // - direction: The direction to move the focus in. // Return Value: - // - - void TerminalPage::_MoveFocus(const FocusDirection& direction) + // - Whether changing the focus succeeded. This allows a keychord to propagate + // to the terminal when no other panes are present (GH#6219) + bool TerminalPage::_MoveFocus(const FocusDirection& direction) { if (const auto terminalTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); - terminalTab->NavigateFocus(direction); + return terminalTab->NavigateFocus(direction); } + return false; + } + + // Method Description: + // - Attempt to swap the positions of the focused pane with another pane. + // See Pane::SwapPane for details. + // Arguments: + // - direction: The direction to move the focused pane in. + // Return Value: + // - true if panes were swapped. + bool TerminalPage::_SwapPane(const FocusDirection& direction) + { + if (const auto terminalTab{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + return terminalTab->SwapPane(direction); + } + return false; } TermControl TerminalPage::_GetActiveControl() @@ -1170,6 +1249,56 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Moves the currently active pane on the currently active tab to the + // specified tab. If the tab index is greater than the number of + // tabs, then a new tab will be created for the pane. Similarly, if a pane + // is the last remaining pane on a tab, that tab will be closed upon moving. + // - No move will occur if the tabIdx is the same as the current tab, or if + // the specified tab is not a host of terminals (such as the settings tab). + // Arguments: + // - tabIdx: The target tab index. + // Return Value: + // - true if the pane was successfully moved to the new tab. + bool TerminalPage::_MovePane(const uint32_t tabIdx) + { + auto focusedTab{ _GetFocusedTabImpl() }; + + if (!focusedTab) + { + return false; + } + + // If we are trying to move from the current tab to the current tab do nothing. + if (_GetFocusedTabIndex() == tabIdx) + { + return false; + } + + // Moving the pane from the current tab might close it, so get the next + // tab before its index changes. + if (_tabs.Size() > tabIdx) + { + auto targetTab = _GetTerminalTabImpl(_tabs.GetAt(tabIdx)); + // if the selected tab is not a host of terminals (e.g. settings) + // don't attempt to add a pane to it. + if (!targetTab) + { + return false; + } + auto pane = focusedTab->DetachPane(); + targetTab->AttachPane(pane); + _SetFocusedTab(*targetTab); + } + else + { + auto pane = focusedTab->DetachPane(); + _CreateNewTabFromPane(pane); + } + + return true; + } + // Method Description: // - Split the focused pane either horizontally or vertically, and place the // given TermControl into the newly created pane. @@ -1200,26 +1329,50 @@ namespace winrt::TerminalApp::implementation return; } + _SplitPane(*focusedTab, splitType, splitMode, splitSize, newTerminalArgs); + } + + // Method Description: + // - Split the focused pane of the given tab, either horizontally or vertically, and place the + // given TermControl into the newly created pane. + // - If splitType == SplitState::None, this method does nothing. + // Arguments: + // - tab: The tab that is going to be split. + // - splitType: one value from the TerminalApp::SplitState enum, indicating how the + // new pane should be split from its parent. + // - splitMode: value from TerminalApp::SplitType enum, indicating the profile to be used in the newly split pane. + // - newTerminalArgs: An object that may contain a blob of parameters to + // control which profile is created and with possible other + // configurations. See CascadiaSettings::BuildSettings for more details. + void TerminalPage::_SplitPane(TerminalTab& tab, + const SplitState splitType, + const SplitType splitMode, + const float splitSize, + const NewTerminalArgs& newTerminalArgs) + { + // Do nothing if we're requesting no split. + if (splitType == SplitState::None) + { + return; + } + try { TerminalSettingsCreateResult controlSettings{ nullptr }; - GUID realGuid; - bool profileFound = false; + Profile profile{ nullptr }; if (splitMode == SplitType::Duplicate) { - std::optional current_guid = focusedTab->GetFocusedProfile(); - if (current_guid) + profile = tab.GetFocusedProfile(); + if (profile) { - profileFound = true; - controlSettings = TerminalSettings::CreateWithProfileByID(_settings, current_guid.value(), *_bindings); - const auto workingDirectory = focusedTab->GetActiveTerminalControl().WorkingDirectory(); + controlSettings = TerminalSettings::CreateWithProfile(_settings, profile, *_bindings); + const auto workingDirectory = tab.GetActiveTerminalControl().WorkingDirectory(); const auto validWorkingDirectory = !workingDirectory.empty(); if (validWorkingDirectory) { controlSettings.DefaultSettings().StartingDirectory(workingDirectory); } - realGuid = current_guid.value(); } // TODO: GH#5047 - In the future, we should get the Profile of // the focused pane, and use that to build a new instance of the @@ -1234,13 +1387,13 @@ namespace winrt::TerminalApp::implementation // connection without keeping an instance of the original Profile // object around. } - if (!profileFound) + if (!profile) { - realGuid = _settings.GetProfileForArgs(newTerminalArgs); + profile = _settings.GetProfileForArgs(newTerminalArgs); controlSettings = TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings); } - const auto controlConnection = _CreateConnectionFromSettings(realGuid, controlSettings.DefaultSettings()); + const auto controlConnection = _CreateConnectionFromSettings(profile, controlSettings.DefaultSettings()); const float contentWidth = ::base::saturated_cast(_tabContent.ActualWidth()); const float contentHeight = ::base::saturated_cast(_tabContent.ActualHeight()); @@ -1249,10 +1402,10 @@ namespace winrt::TerminalApp::implementation auto realSplitType = splitType; if (realSplitType == SplitState::Automatic) { - realSplitType = focusedTab->PreCalculateAutoSplit(availableSpace); + realSplitType = tab.PreCalculateAutoSplit(availableSpace); } - const auto canSplit = focusedTab->PreCalculateCanSplit(realSplitType, splitSize, availableSpace); + const auto canSplit = tab.PreCalculateCanSplit(realSplitType, splitSize, availableSpace); if (!canSplit) { return; @@ -1261,15 +1414,38 @@ namespace winrt::TerminalApp::implementation auto newControl = _InitControl(controlSettings, controlConnection); // Hookup our event handlers to the new terminal - _RegisterTerminalEvents(newControl, *focusedTab); + _RegisterTerminalEvents(newControl); _UnZoomIfNeeded(); - focusedTab->SplitPane(realSplitType, splitSize, realGuid, newControl); + tab.SplitPane(realSplitType, splitSize, profile, newControl); + + // After GH#6586, the control will no longer focus itself + // automatically when it's finished being laid out. Manually focus + // the control here instead. + if (_startupState == StartupState::Initialized) + { + _GetActiveControl().Focus(FocusState::Programmatic); + } } CATCH_LOG(); } + // Method Description: + // - Switches the split orientation of the currently focused pane. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_ToggleSplitOrientation() + { + if (const auto terminalTab{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + terminalTab->ToggleSplitOrientation(); + } + } + // Method Description: // - Attempt to move a separator between panes, as to resize each child on // either size of the separator. See Pane::ResizePane for details. @@ -1833,40 +2009,56 @@ namespace winrt::TerminalApp::implementation _HookupKeyBindings(_settings.ActionMap()); // Refresh UI elements - auto profiles = _settings.ActiveProfiles(); - for (const auto& profile : profiles) + + // Mapping by GUID isn't _excellent_ because the defaults profile doesn't have a stable GUID; however, + // when we stabilize its guid this will become fully safe. + std::unordered_map> profileGuidSettingsMap; + const auto profileDefaults{ _settings.ProfileDefaults() }; + const auto allProfiles{ _settings.AllProfiles() }; + + profileGuidSettingsMap.reserve(allProfiles.Size() + 1); + + // Include the Defaults profile for consideration + profileGuidSettingsMap.insert_or_assign(profileDefaults.Guid(), std::pair{ profileDefaults, nullptr }); + for (const auto& newProfile : allProfiles) { - const auto profileGuid = profile.Guid(); - - try - { - // This can throw an exception if the profileGuid does - // not belong to an actual profile in the list of profiles. - auto settings{ TerminalSettings::CreateWithProfileByID(_settings, profileGuid, *_bindings) }; - - for (auto tab : _tabs) - { - if (auto terminalTab = _GetTerminalTabImpl(tab)) - { - terminalTab->UpdateSettings(settings, profileGuid); - } - } - } - CATCH_LOG(); + // Avoid creating a TerminalSettings right now. They're not totally cheap, and we suspect that users with many + // panes may not be using all of their profiles at the same time. Lazy evaluation is king! + profileGuidSettingsMap.insert_or_assign(newProfile.Guid(), std::pair{ newProfile, nullptr }); } - // GH#2455: If there are any panes with controls that had been - // initialized with a Profile that no longer exists in our list of - // profiles, we'll leave it unmodified. The profile doesn't exist - // anymore, so we can't possibly update its settings. - - // Update the icon of the tab for the currently focused profile in that tab. - // Only do this for TerminalTabs. Other types of tabs won't have multiple panes - // and profiles so the Title and Icon will be set once and only once on init. - for (auto tab : _tabs) + for (const auto& tab : _tabs) { - if (auto terminalTab = _GetTerminalTabImpl(tab)) + if (auto terminalTab{ _GetTerminalTabImpl(tab) }) { + terminalTab->UpdateSettings(); + + // Manually enumerate the panes in each tab; this will let us recycle TerminalSettings + // objects but only have to iterate one time. + terminalTab->GetRootPane()->WalkTree([&](auto&& pane) { + if (const auto profile{ pane->GetProfile() }) + { + const auto found{ profileGuidSettingsMap.find(profile.Guid()) }; + // GH#2455: If there are any panes with controls that had been + // initialized with a Profile that no longer exists in our list of + // profiles, we'll leave it unmodified. The profile doesn't exist + // anymore, so we can't possibly update its settings. + if (found != profileGuidSettingsMap.cend()) + { + auto& pair{ found->second }; + if (!pair.second) + { + pair.second = TerminalSettings::CreateWithProfile(_settings, pair.first, *_bindings); + } + pane->UpdateSettings(pair.second, pair.first); + } + } + return false; + }); + + // Update the icon of the tab for the currently focused profile in that tab. + // Only do this for TerminalTabs. Other types of tabs won't have multiple panes + // and profiles so the Title and Icon will be set once and only once on init. _UpdateTabIcon(*terminalTab); // Force the TerminalTab to re-grab its currently active control's title. @@ -2017,29 +2209,35 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - Gets the taskbar state value from the last active control + // - Get the combined taskbar state for the page. This is the combination of + // all the states of all the tabs, which are themselves a combination of + // all their panes. Taskbar states are given a priority based on the rules + // in: + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate + // under "How the Taskbar Button Chooses the Progress Indicator for a Group" + // Arguments: + // - // Return Value: - // - The taskbar state of the last active control - size_t TerminalPage::GetLastActiveControlTaskbarState() + // - A TaskbarState object representing the combined taskbar state and + // progress percentage of all our tabs. + winrt::TerminalApp::TaskbarState TerminalPage::TaskbarState() const { - if (auto control{ _GetActiveControl() }) - { - return gsl::narrow_cast(control.TaskbarState()); - } - return {}; - } + auto state{ winrt::make() }; - // Method Description: - // - Gets the taskbar progress value from the last active control - // Return Value: - // - The taskbar progress of the last active control - size_t TerminalPage::GetLastActiveControlTaskbarProgress() - { - if (auto control{ _GetActiveControl() }) + for (const auto& tab : _tabs) { - return gsl::narrow_cast(control.TaskbarProgress()); + if (auto tabImpl{ _GetTerminalTabImpl(tab) }) + { + auto tabState{ tabImpl->GetCombinedTaskbarState() }; + // lowest priority wins + if (tabState.Priority() < state.Priority()) + { + state = tabState; + } + } } - return {}; + + return state; } // Method Description: @@ -2325,13 +2523,38 @@ namespace winrt::TerminalApp::implementation return _isAlwaysOnTop; } - void TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection) + HRESULT TerminalPage::_OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection) { - // TODO: GH 9458 will give us more context so we can try to choose a better profile. - _OpenNewTab(nullptr, connection); + // We need to be on the UI thread in order for _OpenNewTab to run successfully. + // HasThreadAccess will return true if we're currently on a UI thread and false otherwise. + // When we're on a COM thread, we'll need to dispatch the calls to the UI thread + // and wait on it hence the locking mechanism. + if (Dispatcher().HasThreadAccess()) + { + // TODO: GH 9458 will give us more context so we can try to choose a better profile. + auto hr = _OpenNewTab(nullptr, connection); - // Request a summon of this window to the foreground - _SummonWindowRequestedHandlers(*this, nullptr); + // Request a summon of this window to the foreground + _SummonWindowRequestedHandlers(*this, nullptr); + + return hr; + } + else + { + til::latch latch{ 1 }; + HRESULT finalVal = S_OK; + + Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [&]() { + finalVal = _OpenNewTab(nullptr, connection); + + _SummonWindowRequestedHandlers(*this, nullptr); + + latch.count_down(); + }); + + latch.wait(); + return finalVal; + } } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 306e44c73..ae3b8c012 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -58,6 +58,8 @@ namespace winrt::TerminalApp::implementation void Create(); + winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e); + hstring Title(); void TitlebarClicked(); @@ -83,8 +85,7 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::IDialogPresenter DialogPresenter() const; void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter); - size_t GetLastActiveControlTaskbarState(); - size_t GetLastActiveControlTaskbarProgress(); + winrt::TerminalApp::TaskbarState TaskbarState() const; void ShowKeyboardServiceWarning(); winrt::hstring KeyboardServiceDisabledText(); @@ -186,12 +187,15 @@ namespace winrt::TerminalApp::implementation void _CreateNewTabFlyout(); void _OpenNewTabDropdown(); - void _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); - void _CreateNewTabFromSettings(GUID profileGuid, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, Microsoft::Terminal::Settings::Model::TerminalSettings settings); + HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + void _CreateNewTabFromPane(std::shared_ptr pane); + void _CreateNewTabWithProfileAndSettings(const Microsoft::Terminal::Settings::Model::Profile& profile, const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Settings::Model::TerminalSettings settings); winrt::fire_and_forget _OpenNewWindow(const bool elevate, const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); + void _OpenNewTerminal(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); + bool _displayingCloseDialog{ false }; void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); void _CommandPaletteButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); @@ -215,12 +219,16 @@ namespace winrt::TerminalApp::implementation void _DuplicateFocusedTab(); void _DuplicateTab(const TerminalTab& tab); + void _SplitTab(TerminalTab& tab); + winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::TabBase tab); void _CloseTabAtIndex(uint32_t index); void _RemoveTab(const winrt::TerminalApp::TabBase& tab); winrt::fire_and_forget _RemoveTabs(const std::vector tabs); - void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term, TerminalTab& hostingTab); + void _InitializeTab(winrt::com_ptr newTabImpl); + void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); + void _RegisterTabEvents(TerminalTab& hostingTab); void _DismissTabContextMenus(); void _FocusCurrentTab(const bool focusAlways); @@ -229,8 +237,10 @@ namespace winrt::TerminalApp::implementation void _ResizeTabContent(const winrt::Windows::Foundation::Size& newSize); void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); - bool _SelectTab(const uint32_t tabIndex); - void _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _SelectTab(uint32_t tabIndex); + bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _MovePane(const uint32_t tabIdx); winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); std::optional _GetFocusedTabIndex() const noexcept; @@ -249,7 +259,13 @@ namespace winrt::TerminalApp::implementation const Microsoft::Terminal::Settings::Model::SplitType splitMode = Microsoft::Terminal::Settings::Model::SplitType::Manual, const float splitSize = 0.5f, const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr); + void _SplitPane(TerminalTab& tab, + const Microsoft::Terminal::Settings::Model::SplitState splitType, + const Microsoft::Terminal::Settings::Model::SplitType splitMode = Microsoft::Terminal::Settings::Model::SplitType::Manual, + const float splitSize = 0.5f, + const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr); void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction); + void _ToggleSplitOrientation(); void _ScrollPage(ScrollDirection scrollDirection); void _ScrollToBufferEdge(ScrollDirection scrollDirection); @@ -331,7 +347,7 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr }; winrt::Microsoft::Terminal::Settings::Model::TerminalSettings _originalSettings{ nullptr }; - void _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); + HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 0d0557822..7d4c580e9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import "TaskbarState.idl"; namespace TerminalApp { @@ -42,8 +43,7 @@ namespace TerminalApp void ShowKeyboardServiceWarning(); String KeyboardServiceDisabledText { get; }; - UInt64 GetLastActiveControlTaskbarState(); - UInt64 GetLastActiveControlTaskbarProgress(); + TaskbarState TaskbarState{ get; }; event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler LastTabClosed; diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 4a0983940..6f415754c 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -25,19 +25,66 @@ namespace winrt namespace winrt::TerminalApp::implementation { - TerminalTab::TerminalTab(const GUID& profile, const TermControl& control) + TerminalTab::TerminalTab(const Profile& profile, const TermControl& control) { _rootPane = std::make_shared(profile, control, true); _rootPane->Id(_nextPaneId); + _activePane = _rootPane; _mruPanes.insert(_mruPanes.begin(), _nextPaneId); ++_nextPaneId; - _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { + _Setup(); + } + + TerminalTab::TerminalTab(std::shared_ptr rootPane) + { + _rootPane = rootPane; + _activePane = nullptr; + + auto firstId = _nextPaneId; + + _rootPane->WalkTree([&](std::shared_ptr pane) { + // update the IDs on each pane + if (pane->_IsLeaf()) + { + pane->Id(_nextPaneId); + _nextPaneId++; + } + // Try to find the pane marked active (if it exists) + if (pane->_lastActive) + { + _activePane = pane; + } + + return false; + }); + + // In case none of the panes were already marked as the focus, just + // focus the first one. + if (_activePane == nullptr) + { + _rootPane->FocusPane(firstId); + _activePane = _rootPane->GetActivePane(); + } + // Set the active control + _mruPanes.insert(_mruPanes.begin(), _activePane->Id().value()); + + _Setup(); + } + + // Method Description: + // - Shared setup for the constructors. Assumed that _rootPane has been set. + // Arguments: + // - + // Return Value: + // - + void TerminalTab::_Setup() + { + _rootClosedToken = _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { _ClosedHandlers(nullptr, nullptr); }); - _activePane = _rootPane; Content(_rootPane->GetRootElement()); _MakeTabViewItem(); @@ -144,19 +191,31 @@ namespace winrt::TerminalApp::implementation // that was last focused. TermControl TerminalTab::GetActiveTerminalControl() const { - return _activePane->GetTerminalControl(); + if (_activePane) + { + return _activePane->GetTerminalControl(); + } + return nullptr; } // Method Description: // - Called after construction of a Tab object to bind event handlers to its - // associated Pane and TermControl object + // associated Pane and TermControl objects // Arguments: - // - control: reference to the TermControl object to bind event to + // - // Return Value: // - - void TerminalTab::Initialize(const TermControl& control) + void TerminalTab::Initialize() { - _BindEventHandlers(control); + _rootPane->WalkTree([&](std::shared_ptr pane) { + // Attach event handlers to each new pane + _AttachEventHandlersToPane(pane); + if (auto control = pane->GetTerminalControl()) + { + _AttachEventHandlersToControl(pane->Id().value(), control); + } + return false; + }); } // Method Description: @@ -177,10 +236,9 @@ namespace winrt::TerminalApp::implementation { lastFocusedControl.Focus(_focusState); - // Update our own progress state, and fire an event signaling + // Update our own progress state. This will fire an event signaling // that our taskbar progress changed. _UpdateProgressState(); - _TaskbarProgressChangedHandlers(lastFocusedControl, nullptr); } // When we gain focus, remove the bell indicator if it is active if (_tabStatus.BellIndicator()) @@ -199,35 +257,19 @@ namespace winrt::TerminalApp::implementation // Return Value: // - nullopt if no children of this tab were the last control to be // focused, else the GUID of the profile of the last control to be focused - std::optional TerminalTab::GetFocusedProfile() const noexcept + Profile TerminalTab::GetFocusedProfile() const noexcept { return _activePane->GetFocusedProfile(); } // Method Description: - // - Called after construction of a Tab object to bind event handlers to its - // associated Pane and TermControl object - // Arguments: - // - control: reference to the TermControl object to bind event to + // - Attempts to update the settings that apply to this tab. + // - Panes are handled elsewhere, by somebody who can establish broader knowledge + // of the settings that apply to all tabs. // Return Value: // - - void TerminalTab::_BindEventHandlers(const TermControl& control) noexcept + void TerminalTab::UpdateSettings() { - _AttachEventHandlersToPane(_rootPane); - _AttachEventHandlersToControl(control); - } - - // Method Description: - // - Attempts to update the settings of this tab's tree of panes. - // Arguments: - // - settings: The new TerminalSettingsCreateResult to apply to any matching controls - // - profile: The GUID of the profile these settings should apply to. - // Return Value: - // - - void TerminalTab::UpdateSettings(const TerminalSettingsCreateResult& settings, const GUID& profile) - { - _rootPane->UpdateSettings(settings, profile); - // The tabWidthMode may have changed, update the header control accordingly _UpdateHeaderControlMaxWidth(); } @@ -406,7 +448,7 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::SplitPane(SplitState splitType, const float splitSize, - const GUID& profile, + const Profile& profile, TermControl& control) { // Make sure to take the ID before calling Split() - Split() will clear out the active pane's ID @@ -426,10 +468,10 @@ namespace winrt::TerminalApp::implementation ++_nextPaneId; } _activePane = first; - _AttachEventHandlersToControl(control); // Add a event handlers to the new panes' GotFocus event. When the pane // gains focus, we'll mark it as the new active pane. + _AttachEventHandlersToControl(second->Id().value(), control); _AttachEventHandlersToPane(first); _AttachEventHandlersToPane(second); @@ -440,6 +482,127 @@ namespace winrt::TerminalApp::implementation _UpdateActivePane(second); } + // Method Description: + // - Removes the currently active pane from this tab. If that was the only + // remaining pane, then the entire tab is closed as well. + // Arguments: + // - + // Return Value: + // - The removed pane, if the remove succeeded. + std::shared_ptr TerminalTab::DetachPane() + { + // if we only have one pane, remove it entirely + // and close this tab + if (_rootPane == _activePane) + { + return DetachRoot(); + } + + // Attempt to remove the active pane from the tree + if (const auto pane = _rootPane->DetachPane(_activePane)) + { + // Just make sure that the remaining pane is marked active + _UpdateActivePane(_rootPane->GetActivePane()); + + return pane; + } + + return nullptr; + } + + // Method Description: + // - Closes this tab and returns the root pane to be used elsewhere. + // Arguments: + // - + // Return Value: + // - The root pane. + std::shared_ptr TerminalTab::DetachRoot() + { + // remove the closed event handler since we are closing the tab + // manually. + _rootPane->Closed(_rootClosedToken); + auto p = _rootPane; + p->WalkTree([](auto pane) { + pane->_PaneDetachedHandlers(pane); + return false; + }); + + // Clean up references and close the tab + _rootPane = nullptr; + _activePane = nullptr; + Content(nullptr); + _ClosedHandlers(nullptr, nullptr); + + return p; + } + + // Method Description: + // - Add an arbitrary pane to this tab. This will be added as a split on the + // currently active pane. + // Arguments: + // - pane: The pane to add. + // Return Value: + // - + void TerminalTab::AttachPane(std::shared_ptr pane) + { + // Add the new event handlers to the new pane(s) + // and update their ids. + pane->WalkTree([&](auto p) { + _AttachEventHandlersToPane(p); + if (p->_IsLeaf()) + { + p->Id(_nextPaneId); + _nextPaneId++; + } + if (auto control = p->GetTerminalControl()) + { + _AttachEventHandlersToControl(p->Id().value(), control); + } + return false; + }); + + // pass the old id to the new child + const auto previousId = _activePane->Id(); + + // Add the new pane as an automatic split on the active pane. + auto first = _activePane->AttachPane(pane, SplitState::Automatic); + + // under current assumptions this condition should always be true. + if (previousId) + { + first->Id(previousId.value()); + } + else + { + first->Id(_nextPaneId); + ++_nextPaneId; + } + + // Update with event handlers on the new child. + _activePane = first; + _AttachEventHandlersToPane(first); + + // Make sure that we have the right pane set as the active pane + pane->WalkTree([&](auto p) { + if (p->_lastActive) + { + _UpdateActivePane(p); + return true; + } + return false; + }); + } + + // Method Description: + // - Find the currently active pane, and then switch the split direction of + // its parent. E.g. switch from Horizontal to Vertical. + // Return Value: + // - + void TerminalTab::ToggleSplitOrientation() + { + _rootPane->ToggleSplitOrientation(); + } + // Method Description: // - See Pane::CalcSnappedDimension float TerminalTab::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const @@ -481,22 +644,68 @@ namespace winrt::TerminalApp::implementation // Arguments: // - direction: The direction to move the focus in. // Return Value: - // - - void TerminalTab::NavigateFocus(const FocusDirection& direction) + // - Whether changing the focus succeeded. This allows a keychord to propagate + // to the terminal when no other panes are present (GH#6219) + bool TerminalTab::NavigateFocus(const FocusDirection& direction) { if (direction == FocusDirection::Previous) { + if (_mruPanes.size() < 2) + { + return false; + } // To get to the previous pane, get the id of the previous pane and focus to that - _rootPane->FocusPane(_mruPanes.at(1)); + return _rootPane->FocusPane(_mruPanes.at(1)); } else { // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. - _rootPane->NavigateFocus(direction); + if (auto newFocus = _rootPane->NavigateDirection(_activePane, direction)) + { + return _rootPane->FocusPane(newFocus); + } + + return false; } } + // Method Description: + // - Attempts to swap the location of the focused pane with another pane + // according to direction. When there are multiple adjacent panes it will + // select the first one (top-left-most). + // Arguments: + // - direction: The direction to move the pane in. + // Return Value: + // - true if two panes were swapped. + bool TerminalTab::SwapPane(const FocusDirection& direction) + { + if (direction == FocusDirection::Previous) + { + if (_mruPanes.size() < 2) + { + return false; + } + if (auto lastPane = _rootPane->FindPane(_mruPanes.at(1))) + { + return _rootPane->SwapPanes(_activePane, lastPane); + } + } + else + { + // NOTE: This _must_ be called on the root pane, so that it can propagate + // throughout the entire tree. + if (auto neighbor = _rootPane->NavigateDirection(_activePane, direction)) + { + return _rootPane->SwapPanes(_activePane, neighbor); + } + + return false; + } + + return false; + } + bool TerminalTab::FocusPane(const uint32_t id) { return _rootPane->FocusPane(id); @@ -506,7 +715,10 @@ namespace winrt::TerminalApp::implementation // - Prepares this tab for being removed from the UI hierarchy by shutting down all active connections. void TerminalTab::Shutdown() { - _rootPane->Shutdown(); + if (_rootPane) + { + _rootPane->Shutdown(); + } } // Method Description: @@ -551,6 +763,34 @@ namespace winrt::TerminalApp::implementation _headerControl.BeginRename(); } + // Method Description: + // - Removes any event handlers set by the tab on the given pane's control. + // The pane's ID is the most stable identifier for a given control, because + // the control itself doesn't have a particular ID and its pointer is + // unstable since it is moved when panes split. + // Arguments: + // - paneId: The ID of the pane that contains the given control. + // - control: the control to remove events from. + // Return Value: + // - + void TerminalTab::_DetachEventHandlersFromControl(const uint32_t paneId, const TermControl& control) + { + auto it = _controlEvents.find(paneId); + if (it != _controlEvents.end()) + { + auto& events = it->second; + + control.TitleChanged(events.titleToken); + control.FontSizeChanged(events.fontToken); + control.TabColorChanged(events.colorToken); + control.SetTaskbarProgress(events.taskbarToken); + control.ReadOnlyChanged(events.readOnlyToken); + control.FocusFollowMouseRequested(events.focusToken); + + _controlEvents.erase(paneId); + } + } + // Method Description: // - Register any event handlers that we may need with the given TermControl. // This should be called on each and every TermControl that we add to the tree @@ -558,15 +798,17 @@ namespace winrt::TerminalApp::implementation // * notify us when the control's title changed, so we can update our own // title (if necessary) // Arguments: + // - paneId: the ID of the pane that this control belongs to. // - control: the TermControl to add events to. // Return Value: // - - void TerminalTab::_AttachEventHandlersToControl(const TermControl& control) + void TerminalTab::_AttachEventHandlersToControl(const uint32_t paneId, const TermControl& control) { auto weakThis{ get_weak() }; auto dispatcher = TabViewItem().Dispatcher(); + ControlEventTokens events{}; - control.TitleChanged([weakThis](auto&&, auto&&) { + events.titleToken = control.TitleChanged([weakThis](auto&&, auto&&) { // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) { @@ -581,16 +823,16 @@ namespace winrt::TerminalApp::implementation // On the latter event, we tell the root pane to resize itself so that its descendants // (including ourself) can properly snap to character grids. In future, we may also // want to do that on regular font changes. - control.FontSizeChanged([this](const int /* fontWidth */, - const int /* fontHeight */, - const bool isInitialChange) { + events.fontToken = control.FontSizeChanged([this](const int /* fontWidth */, + const int /* fontHeight */, + const bool isInitialChange) { if (isInitialChange) { _rootPane->Relayout(); } }); - control.TabColorChanged([weakThis](auto&&, auto&&) { + events.colorToken = control.TabColorChanged([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { // The control's tabColor changed, but it is not necessarily the @@ -600,7 +842,7 @@ namespace winrt::TerminalApp::implementation } }); - control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget { + events.taskbarToken = control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget { co_await winrt::resume_foreground(dispatcher); // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) @@ -609,14 +851,14 @@ namespace winrt::TerminalApp::implementation } }); - control.ReadOnlyChanged([weakThis](auto&&, auto&&) { + events.readOnlyToken = control.ReadOnlyChanged([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { tab->_RecalculateAndApplyReadOnly(); } }); - control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) { + events.focusToken = control.FocusFollowMouseRequested([weakThis](auto&& sender, auto&&) { if (const auto tab{ weakThis.get() }) { if (tab->_focusState != FocusState::Unfocused) @@ -628,6 +870,31 @@ namespace winrt::TerminalApp::implementation } } }); + + _controlEvents[paneId] = events; + } + + // Method Description: + // - Get the combined taskbar state for the tab. This is the combination of + // all the states of all our panes. Taskbar states are given a priority + // based on the rules in: + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate + // under "How the Taskbar Button Chooses the Progress Indicator for a + // Group" + // Arguments: + // - + // Return Value: + // - A TaskbarState object representing the combined taskbar state and + // progress percentage of all our panes. + winrt::TerminalApp::TaskbarState TerminalTab::GetCombinedTaskbarState() const + { + std::vector states; + if (_rootPane) + { + _rootPane->CollectTaskbarStates(states); + } + return states.empty() ? winrt::make() : + *std::min_element(states.begin(), states.end(), TerminalApp::implementation::TaskbarState::ComparePriority); } // Method Description: @@ -645,37 +912,39 @@ namespace winrt::TerminalApp::implementation // - void TerminalTab::_UpdateProgressState() { - if (const auto& activeControl{ GetActiveTerminalControl() }) - { - const auto taskbarState = activeControl.TaskbarState(); - // The progress of the control changed, but not necessarily the progress of the tab. - // Set the tab's progress ring to the active pane's progress - if (taskbarState > 0) - { - if (taskbarState == 3) - { - // 3 is the indeterminate state, set the progress ring as such - _tabStatus.IsProgressRingIndeterminate(true); - } - else - { - // any non-indeterminate state has a value, set the progress ring as such - _tabStatus.IsProgressRingIndeterminate(false); + const auto state{ GetCombinedTaskbarState() }; - const auto progressValue = gsl::narrow(activeControl.TaskbarProgress()); - _tabStatus.ProgressValue(progressValue); - } - // Hide the tab icon (the progress ring is placed over it) - HideIcon(true); - _tabStatus.IsProgressRingActive(true); + const auto taskbarState = state.State(); + // The progress of the control changed, but not necessarily the progress of the tab. + // Set the tab's progress ring to the active pane's progress + if (taskbarState > 0) + { + if (taskbarState == 3) + { + // 3 is the indeterminate state, set the progress ring as such + _tabStatus.IsProgressRingIndeterminate(true); } else { - // Show the tab icon - HideIcon(false); - _tabStatus.IsProgressRingActive(false); + // any non-indeterminate state has a value, set the progress ring as such + _tabStatus.IsProgressRingIndeterminate(false); + + const auto progressValue = gsl::narrow(state.Progress()); + _tabStatus.ProgressValue(progressValue); } + // Hide the tab icon (the progress ring is placed over it) + HideIcon(true); + _tabStatus.IsProgressRingActive(true); } + else + { + // Show the tab icon + HideIcon(false); + _tabStatus.IsProgressRingActive(false); + } + + // fire an event signaling that our taskbar progress changed. + _TaskbarProgressChangedHandlers(nullptr, nullptr); } // Method Description: @@ -732,7 +1001,7 @@ namespace winrt::TerminalApp::implementation auto weakThis{ get_weak() }; std::weak_ptr weakPane{ pane }; - pane->GotFocus([weakThis](std::shared_ptr sender) { + auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr sender) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -752,7 +1021,7 @@ namespace winrt::TerminalApp::implementation } }); - pane->LostFocus([weakThis](std::shared_ptr /*sender*/) { + auto lostFocusToken = pane->LostFocus([weakThis](std::shared_ptr /*sender*/) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -766,7 +1035,7 @@ namespace winrt::TerminalApp::implementation // Add a Closed event handler to the Pane. If the pane closes out from // underneath us, and it's zoomed, we want to be able to make sure to // update our state accordingly to un-zoom that pane. See GH#7252. - pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget { + auto closedToken = pane->Closed([weakThis, weakPane](auto&& /*s*/, auto && /*e*/) -> winrt::fire_and_forget { if (auto tab{ weakThis.get() }) { if (tab->_zoomedPane) @@ -791,7 +1060,7 @@ namespace winrt::TerminalApp::implementation }); // Add a PaneRaiseBell event handler to the Pane - pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) { + auto bellToken = pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) { if (auto tab{ weakThis.get() }) { if (visual) @@ -813,6 +1082,40 @@ namespace winrt::TerminalApp::implementation } } }); + + // box the event token so that we can give a reference to it in the + // event handler. + auto detachedToken = std::make_shared(); + // Add a Detached event handler to the Pane to clean up tab state + // and other event handlers when a pane is removed from this tab. + *detachedToken = pane->Detached([weakThis, weakPane, gotFocusToken, lostFocusToken, closedToken, bellToken, detachedToken](std::shared_ptr /*sender*/) { + // Make sure we do this at most once + if (auto pane{ weakPane.lock() }) + { + pane->Detached(*detachedToken); + pane->GotFocus(gotFocusToken); + pane->LostFocus(lostFocusToken); + pane->Closed(closedToken); + pane->PaneRaiseBell(bellToken); + + if (auto tab{ weakThis.get() }) + { + if (auto control = pane->GetTerminalControl()) + { + tab->_DetachEventHandlersFromControl(pane->Id().value(), control); + } + + for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i) + { + if (*i == pane->Id()) + { + tab->_mruPanes.erase(i); + break; + } + } + } + } + }); } // Method Description: @@ -891,12 +1194,30 @@ namespace winrt::TerminalApp::implementation duplicateTabMenuItem.Icon(duplicateTabSymbol); } + Controls::MenuFlyoutItem splitTabMenuItem; + { + // "Split Tab" + Controls::FontIcon splitTabSymbol; + splitTabSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); + splitTabSymbol.Glyph(L"\xF246"); // ViewDashboard + + splitTabMenuItem.Click([weakThis](auto&&, auto&&) { + if (auto tab{ weakThis.get() }) + { + tab->_SplitTabRequestedHandlers(); + } + }); + splitTabMenuItem.Text(RS_(L"SplitTabText")); + splitTabMenuItem.Icon(splitTabSymbol); + } + // Build the menu Controls::MenuFlyout contextMenuFlyout; Controls::MenuFlyoutSeparator menuSeparator; contextMenuFlyout.Items().Append(chooseColorMenuItem); contextMenuFlyout.Items().Append(renameTabMenuItem); contextMenuFlyout.Items().Append(duplicateTabMenuItem); + contextMenuFlyout.Items().Append(splitTabMenuItem); contextMenuFlyout.Items().Append(menuSeparator); // GH#5750 - When the context menu is dismissed with ESC, toss the focus @@ -1188,6 +1509,7 @@ namespace winrt::TerminalApp::implementation EnterZoom(); } } + void TerminalTab::EnterZoom() { _zoomedPane = _activePane; @@ -1269,4 +1591,5 @@ namespace winrt::TerminalApp::implementation DEFINE_EVENT(TerminalTab, ColorCleared, _colorCleared, winrt::delegate<>); DEFINE_EVENT(TerminalTab, TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>); DEFINE_EVENT(TerminalTab, DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>); + DEFINE_EVENT(TerminalTab, SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>); } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 7167f74a6..4952fad45 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -21,23 +21,29 @@ namespace winrt::TerminalApp::implementation struct TerminalTab : TerminalTabT { public: - TerminalTab(const GUID& profile, const winrt::Microsoft::Terminal::Control::TermControl& control); + TerminalTab(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, const winrt::Microsoft::Terminal::Control::TermControl& control); + TerminalTab(std::shared_ptr rootPane); // Called after construction to perform the necessary setup, which relies on weak_ptr - void Initialize(const winrt::Microsoft::Terminal::Control::TermControl& control); + void Initialize(); winrt::Microsoft::Terminal::Control::TermControl GetActiveTerminalControl() const; - std::optional GetFocusedProfile() const noexcept; + winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile() const noexcept; void Focus(winrt::Windows::UI::Xaml::FocusState focusState) override; winrt::fire_and_forget Scroll(const int delta); + std::shared_ptr DetachRoot(); + std::shared_ptr DetachPane(); + void AttachPane(std::shared_ptr pane); + void SplitPane(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, const float splitSize, - const GUID& profile, + const winrt::Microsoft::Terminal::Settings::Model::Profile& profile, winrt::Microsoft::Terminal::Control::TermControl& control); + void ToggleSplitOrientation(); winrt::fire_and_forget UpdateIcon(const winrt::hstring iconPath); winrt::fire_and_forget HideIcon(const bool hide); @@ -52,10 +58,11 @@ namespace winrt::TerminalApp::implementation void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void ResizePane(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction); - void NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool SwapPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool FocusPane(const uint32_t id); - void UpdateSettings(const Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, const GUID& profile); + void UpdateSettings(); winrt::fire_and_forget UpdateTitle(); void Shutdown() override; @@ -81,6 +88,9 @@ namespace winrt::TerminalApp::implementation void TogglePaneReadOnly(); std::shared_ptr GetActivePane() const; + winrt::TerminalApp::TaskbarState GetCombinedTaskbarState() const; + + std::shared_ptr GetRootPane() const { return _rootPane; } winrt::TerminalApp::TerminalTabStatus TabStatus() { @@ -92,12 +102,14 @@ namespace winrt::TerminalApp::implementation DECLARE_EVENT(ColorCleared, _colorCleared, winrt::delegate<>); DECLARE_EVENT(TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>); DECLARE_EVENT(DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>); + DECLARE_EVENT(SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>); TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable); private: std::shared_ptr _rootPane{ nullptr }; std::shared_ptr _activePane{ nullptr }; std::shared_ptr _zoomedPane{ nullptr }; + winrt::hstring _lastIconPath{}; winrt::TerminalApp::ColorPickupFlyout _tabColorPickup{}; std::optional _themeTabColor{}; @@ -105,6 +117,19 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TabHeaderControl _headerControl{}; winrt::TerminalApp::TerminalTabStatus _tabStatus{}; + struct ControlEventTokens + { + winrt::event_token titleToken; + winrt::event_token fontToken; + winrt::event_token colorToken; + winrt::event_token taskbarToken; + winrt::event_token readOnlyToken; + winrt::event_token focusToken; + }; + std::unordered_map _controlEvents; + + winrt::event_token _rootClosedToken{}; + std::vector _mruPanes; uint32_t _nextPaneId{ 0 }; @@ -117,6 +142,8 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::ShortcutActionDispatch _dispatch; + void _Setup(); + std::optional _bellIndicatorTimer; void _BellIndicatorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); @@ -129,9 +156,8 @@ namespace winrt::TerminalApp::implementation void _RefreshVisualState(); - void _BindEventHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control) noexcept; - - void _AttachEventHandlersToControl(const winrt::Microsoft::Terminal::Control::TermControl& control); + void _DetachEventHandlersFromControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); + void _AttachEventHandlersToControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); void _AttachEventHandlersToPane(std::shared_ptr pane); void _UpdateActivePane(std::shared_ptr pane); diff --git a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj index 5f9d3f7c7..953280251 100644 --- a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj +++ b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj @@ -89,13 +89,13 @@ - + 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/src/cascadia/TerminalApp/packages.config b/src/cascadia/TerminalApp/packages.config index a54ab2a0e..e3bd479b1 100644 --- a/src/cascadia/TerminalApp/packages.config +++ b/src/cascadia/TerminalApp/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index 299d7de63..1f84f821e 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -66,6 +66,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider); #include #include +#include #include #include diff --git a/src/cascadia/TerminalAzBridge/main.cpp b/src/cascadia/TerminalAzBridge/main.cpp index df51677d7..3137109e9 100644 --- a/src/cascadia/TerminalAzBridge/main.cpp +++ b/src/cascadia/TerminalAzBridge/main.cpp @@ -96,7 +96,11 @@ int wmain(int /*argc*/, wchar_t** /*argv*/) const auto size = GetConsoleScreenSize(conOut); - AzureConnection azureConn{ gsl::narrow_cast(size.Y), gsl::narrow_cast(size.X) }; + AzureConnection azureConn{}; + winrt::Windows::Foundation::Collections::ValueSet vs{}; + vs.Insert(L"initialRows", winrt::Windows::Foundation::PropertyValue::CreateUInt32(gsl::narrow_cast(size.Y))); + vs.Insert(L"initialCols", winrt::Windows::Foundation::PropertyValue::CreateUInt32(gsl::narrow_cast(size.X))); + azureConn.Initialize(vs); const auto state = RunConnectionToCompletion(azureConn, conOut, conIn); diff --git a/src/cascadia/TerminalAzBridge/pch.h b/src/cascadia/TerminalAzBridge/pch.h index 8a1dc650f..5b8835166 100644 --- a/src/cascadia/TerminalAzBridge/pch.h +++ b/src/cascadia/TerminalAzBridge/pch.h @@ -33,6 +33,7 @@ Abstract: #include #include +#include #include #include diff --git a/src/cascadia/TerminalConnection/AzureConnection.cpp b/src/cascadia/TerminalConnection/AzureConnection.cpp index 2798896a2..d5fe0bdd7 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.cpp +++ b/src/cascadia/TerminalConnection/AzureConnection.cpp @@ -71,11 +71,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation return (AzureClientID != L"0"); } - AzureConnection::AzureConnection(const uint32_t initialRows, const uint32_t initialCols) : - _initialRows{ initialRows }, - _initialCols{ initialCols }, - _expiry{} + void AzureConnection::Initialize(const Windows::Foundation::Collections::ValueSet& settings) { + if (settings) + { + _initialRows = winrt::unbox_value_or(settings.TryLookup(L"initialRows").try_as(), _initialRows); + _initialCols = winrt::unbox_value_or(settings.TryLookup(L"initialCols").try_as(), _initialCols); + } } // Method description: diff --git a/src/cascadia/TerminalConnection/AzureConnection.h b/src/cascadia/TerminalConnection/AzureConnection.h index b2afd99ab..7c51294f8 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.h +++ b/src/cascadia/TerminalConnection/AzureConnection.h @@ -21,7 +21,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { static winrt::guid ConnectionType() noexcept; static bool IsAzureConnectionAvailable() noexcept; - AzureConnection(const uint32_t rows, const uint32_t cols); + + AzureConnection() = default; + void Initialize(const Windows::Foundation::Collections::ValueSet& settings); void Start(); void WriteInput(hstring const& data); diff --git a/src/cascadia/TerminalConnection/AzureConnection.idl b/src/cascadia/TerminalConnection/AzureConnection.idl index 6bd6e4930..ecaface97 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.idl +++ b/src/cascadia/TerminalConnection/AzureConnection.idl @@ -10,7 +10,7 @@ namespace Microsoft.Terminal.TerminalConnection static Guid ConnectionType { get; }; static Boolean IsAzureConnectionAvailable(); - AzureConnection(UInt32 rows, UInt32 columns); + AzureConnection(); }; } diff --git a/src/cascadia/TerminalConnection/CTerminalHandoff.cpp b/src/cascadia/TerminalConnection/CTerminalHandoff.cpp index 0138921fd..5023a8188 100644 --- a/src/cascadia/TerminalConnection/CTerminalHandoff.cpp +++ b/src/cascadia/TerminalConnection/CTerminalHandoff.cpp @@ -11,6 +11,8 @@ using namespace Microsoft::WRL; static NewHandoffFunction _pfnHandoff = nullptr; // The registration ID of the class object for clean up later static DWORD g_cTerminalHandoffRegistration = 0; +// Mutex so we only do start/stop/establish one at a time. +static std::shared_mutex _mtx; // Routine Description: // - Starts listening for TerminalHandoff requests by registering @@ -19,9 +21,11 @@ static DWORD g_cTerminalHandoffRegistration = 0; // - pfnHandoff - Function to callback when a handoff is received // Return Value: // - S_OK, E_NOT_VALID_STATE (start called when already started) or relevant COM registration error. -HRESULT CTerminalHandoff::s_StartListening(NewHandoffFunction pfnHandoff) noexcept +HRESULT CTerminalHandoff::s_StartListening(NewHandoffFunction pfnHandoff) try { + std::unique_lock lock{ _mtx }; + RETURN_HR_IF(E_NOT_VALID_STATE, _pfnHandoff != nullptr); const auto classFactory = Make>(); @@ -31,7 +35,7 @@ try ComPtr unk; RETURN_IF_FAILED(classFactory.As(&unk)); - RETURN_IF_FAILED(CoRegisterClassObject(__uuidof(CTerminalHandoff), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &g_cTerminalHandoffRegistration)); + RETURN_IF_FAILED(CoRegisterClassObject(__uuidof(CTerminalHandoff), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE, &g_cTerminalHandoffRegistration)); _pfnHandoff = pfnHandoff; @@ -46,8 +50,10 @@ CATCH_RETURN() // - // Return Value: // - S_OK, E_NOT_VALID_STATE (stop called when not started), or relevant COM class revoke error -HRESULT CTerminalHandoff::s_StopListening() noexcept +HRESULT CTerminalHandoff::s_StopListening() { + std::unique_lock lock{ _mtx }; + RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff); _pfnHandoff = nullptr; @@ -91,10 +97,19 @@ static HRESULT _duplicateHandle(const HANDLE in, HANDLE& out) noexcept // - E_NOT_VALID_STATE if a event handler is not registered before calling. `::DuplicateHandle` // error codes if we cannot manage to make our own copy of handles to retain. Or S_OK/error // from the registered handler event function. -HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) noexcept +HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client) { + // Stash a local copy of _pfnHandoff before we stop listening. + auto localPfnHandoff = _pfnHandoff; + + // Because we are REGCLS_SINGLEUSE... we need to `CoRevokeClassObject` after we handle this ONE call. + // COM does not automatically clean that up for us. We must do it. + s_StopListening(); + + std::unique_lock lock{ _mtx }; + // Report an error if no one registered a handoff function before calling this. - RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _pfnHandoff); + RETURN_HR_IF_NULL(E_NOT_VALID_STATE, localPfnHandoff); // Duplicate the handles from what we received. // The contract with COM specifies that any HANDLEs we receive from the caller belong @@ -108,5 +123,5 @@ HRESULT CTerminalHandoff::EstablishPtyHandoff(HANDLE in, HANDLE out, HANDLE sign RETURN_IF_FAILED(_duplicateHandle(client, client)); // Call registered handler from when we started listening. - return _pfnHandoff(in, out, signal, ref, server, client); + return localPfnHandoff(in, out, signal, ref, server, client); } diff --git a/src/cascadia/TerminalConnection/CTerminalHandoff.h b/src/cascadia/TerminalConnection/CTerminalHandoff.h index 2e173e0a3..2ba644f41 100644 --- a/src/cascadia/TerminalConnection/CTerminalHandoff.h +++ b/src/cascadia/TerminalConnection/CTerminalHandoff.h @@ -37,12 +37,12 @@ struct __declspec(uuid(__CLSID_CTerminalHandoff)) HANDLE signal, HANDLE ref, HANDLE server, - HANDLE client) noexcept override; + HANDLE client) override; #pragma endregion - static HRESULT s_StartListening(NewHandoffFunction pfnHandoff) noexcept; - static HRESULT s_StopListening() noexcept; + static HRESULT s_StartListening(NewHandoffFunction pfnHandoff); + static HRESULT s_StopListening(); }; // Disable warnings from the CoCreatableClass macro as the value it provides for diff --git a/src/cascadia/TerminalConnection/ConnectionInformation.cpp b/src/cascadia/TerminalConnection/ConnectionInformation.cpp new file mode 100644 index 000000000..5ae37fad6 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConnectionInformation.cpp @@ -0,0 +1,66 @@ +#include "pch.h" +#include "ConnectionInformation.h" +#include "ConnectionInformation.g.cpp" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + ConnectionInformation::ConnectionInformation(hstring const& className, + const Windows::Foundation::Collections::ValueSet& settings) : + _ClassName{ className }, + _Settings{ settings } + { + } + + // Function Description: + // - Create an instance of the connection specified in the + // ConnectionInformation, and Initialize it. + // - This static method allows the content process to create a connection + // from information that lives in the window process. + // Arguments: + // - info: A ConnectionInformation object that possibly lives out-of-proc, + // containing the name of the WinRT class we should activate for this + // connection, and a bag of setting to use to initialize that object. + // Return Value: + // - + TerminalConnection::ITerminalConnection ConnectionInformation::CreateConnection(TerminalConnection::ConnectionInformation info) + try + { + Windows::Foundation::IInspectable inspectable{}; + + const auto name = static_cast(winrt::get_abi(info.ClassName())); + const auto pointer = winrt::put_abi(inspectable); + +#pragma warning(push) +#pragma warning(disable : 26490) + // C++/WinRT just loves it's void**, nothing we can do here _except_ reinterpret_cast + ::IInspectable** raw = reinterpret_cast<::IInspectable**>(pointer); +#pragma warning(pop) + + // RoActivateInstance() will try to create an instance of the object, + // who's fully qualified name is the string in Name(). + // + // The class has to be activatable. For the Terminal, this is easy + // enough - we're not hosting anything that's not already in our + // manifest, or living as a .dll & .winmd SxS. + // + // When we get to extensions (GH#4000), we may want to revisit. + if (LOG_IF_FAILED(RoActivateInstance(name, raw))) + { + return nullptr; + } + + // Now that thing we made, make sure it's actually a ITerminalConnection + if (const auto connection{ inspectable.try_as() }) + { + // Initialize it, and return it. + connection.Initialize(info.Settings()); + return connection; + } + return nullptr; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + return nullptr; + } +} diff --git a/src/cascadia/TerminalConnection/ConnectionInformation.h b/src/cascadia/TerminalConnection/ConnectionInformation.h new file mode 100644 index 000000000..75bdd23d7 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConnectionInformation.h @@ -0,0 +1,43 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- ConnectionInformation.h + +Abstract: +- This is a helper object for storing both the name of a type of connection, and + a bag of settings to use to initialize that connection. +- This helper is used primarily in cross-proc scenarios, to allow the window + process to tell the content process the name of the connection type it wants + created, and how to set that connection up. This is done so the connection can + live entirely in the content process, without having to go through the window + process at all. +--*/ + +#pragma once +#include "../inc/cppwinrt_utils.h" +#include "ConnectionInformation.g.h" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct ConnectionInformation : ConnectionInformationT + { + ConnectionInformation(hstring const& className, + const Windows::Foundation::Collections::ValueSet& settings); + + static TerminalConnection::ITerminalConnection CreateConnection(TerminalConnection::ConnectionInformation info); + + winrt::hstring ClassName() const { return _ClassName; } + void ClassName(const winrt::hstring& value) { _ClassName = value; } + + WINRT_PROPERTY(Windows::Foundation::Collections::ValueSet, Settings); + + private: + winrt::hstring _ClassName{}; + }; +} +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + BASIC_FACTORY(ConnectionInformation); +} diff --git a/src/cascadia/TerminalConnection/ConnectionInformation.idl b/src/cascadia/TerminalConnection/ConnectionInformation.idl new file mode 100644 index 000000000..832336eaf --- /dev/null +++ b/src/cascadia/TerminalConnection/ConnectionInformation.idl @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] runtimeclass ConnectionInformation + { + ConnectionInformation(String className, Windows.Foundation.Collections.ValueSet settings); + String ClassName { get; }; + Windows.Foundation.Collections.ValueSet Settings { get; }; + + static ITerminalConnection CreateConnection(ConnectionInformation info); + } + +} diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index a5666f782..d82b6be0a 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -57,6 +57,72 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation return S_OK; } + // Function Description: + // - Promotes a starting directory provided to a WSL invocation to a commandline argument. + // This is necessary because WSL has some modicum of support for linux-side directories (!) which + // CreateProcess never will. + static std::tuple _tryMangleStartingDirectoryForWSL(std::wstring_view commandLine, std::wstring_view startingDirectory) + { + do + { + if (startingDirectory.size() > 0 && commandLine.size() >= 3) + { // "wsl" is three characters; this is a safe bet. no point in doing it if there's no starting directory though! + // Find the first space, quote or the end of the string -- we'll look for wsl before that. + const auto terminator{ commandLine.find_first_of(LR"(" )", 1) }; // look past the first character in case it starts with " + const auto start{ til::at(commandLine, 0) == L'"' ? 1 : 0 }; + const std::filesystem::path executablePath{ commandLine.substr(start, terminator - start) }; + const auto executableFilename{ executablePath.filename().wstring() }; + if (executableFilename == L"wsl" || executableFilename == L"wsl.exe") + { + // We've got a WSL -- let's just make sure it's the right one. + if (executablePath.has_parent_path()) + { + std::wstring systemDirectory{}; + if (FAILED(wil::GetSystemDirectoryW(systemDirectory))) + { + break; // just bail out. + } + if (executablePath.parent_path().wstring() != systemDirectory) + { + break; // it wasn't in system32! + } + } + else + { + // assume that unqualified WSL is the one in system32 (minor danger) + } + + const auto arguments{ terminator == std::wstring_view::npos ? std::wstring_view{} : commandLine.substr(terminator + 1) }; + if (arguments.find(L"--cd") != std::wstring_view::npos) + { + break; // they've already got a --cd! + } + + const auto tilde{ arguments.find_first_of(L'~') }; + if (tilde != std::wstring_view::npos) + { + if (tilde + 1 == arguments.size() || til::at(arguments, tilde + 1) == L' ') + { + // We want to suppress --cd if they have added a bare ~ to their commandline (they conflict). + break; + } + // Tilde followed by non-space should be okay (like, wsl -d Debian ~/blah.sh) + } + + return { + fmt::format(LR"("{}" --cd "{}" {})", executablePath.wstring(), startingDirectory, arguments), + std::wstring{} + }; + } + } + } while (false); + + return { + std::wstring{ commandLine }, + std::wstring{ startingDirectory } + }; + } + // Function Description: // - launches the client application attached to the new pseudoconsole HRESULT ConptyConnection::_LaunchAttachedClient() noexcept @@ -116,17 +182,23 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // add additional WT env vars like WT_SETTINGS, WT_DEFAULTS and WT_PROFILE_ID for (auto item : _environment) { - auto key = item.Key(); - auto value = item.Value(); - - // avoid clobbering WSLENV - if (std::wstring_view{ key } == L"WSLENV") + try { - auto current = environment[L"WSLENV"]; - value = current + L":" + value; - } + auto key = item.Key(); + // This will throw if the value isn't a string. If that + // happens, then just skip this entry. + auto value = winrt::unbox_value(item.Value()); - environment.insert_or_assign(key.c_str(), value.c_str()); + // avoid clobbering WSLENV + if (std::wstring_view{ key } == L"WSLENV") + { + auto current = environment[L"WSLENV"]; + value = current + L":" + value; + } + + environment.insert_or_assign(key.c_str(), value.c_str()); + } + CATCH_LOG(); } } @@ -157,11 +229,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation siEx.StartupInfo.lpTitle = mutableTitle.data(); } - const wchar_t* const startingDirectory = _startingDirectory.size() > 0 ? _startingDirectory.c_str() : nullptr; + auto [newCommandLine, newStartingDirectory] = _tryMangleStartingDirectoryForWSL(cmdline, _startingDirectory); + const wchar_t* const startingDirectory = newStartingDirectory.size() > 0 ? newStartingDirectory.c_str() : nullptr; RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( nullptr, - cmdline.data(), + newCommandLine.data(), nullptr, // lpProcessAttributes nullptr, // lpThreadAttributes false, // bInheritHandles @@ -219,24 +292,54 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation _piClient.hProcess = hClientProcess; } - ConptyConnection::ConptyConnection(const hstring& commandline, - const hstring& startingDirectory, - const hstring& startingTitle, - const Windows::Foundation::Collections::IMapView& environment, - const uint32_t initialRows, - const uint32_t initialCols, - const guid& initialGuid) : - _initialRows{ initialRows }, - _initialCols{ initialCols }, - _commandline{ commandline }, - _startingDirectory{ startingDirectory }, - _startingTitle{ startingTitle }, - _environment{ environment }, - _guid{ initialGuid }, - _u8State{}, - _u16Str{}, - _buffer{} + // Function Description: + // - Helper function for constructing a ValueSet that we can use to get our settings from. + Windows::Foundation::Collections::ValueSet ConptyConnection::CreateSettings(const winrt::hstring& cmdline, + const winrt::hstring& startingDirectory, + const winrt::hstring& startingTitle, + Windows::Foundation::Collections::IMapView const& environment, + uint32_t rows, + uint32_t columns, + winrt::guid const& guid) { + Windows::Foundation::Collections::ValueSet vs{}; + + vs.Insert(L"commandline", Windows::Foundation::PropertyValue::CreateString(cmdline)); + vs.Insert(L"startingDirectory", Windows::Foundation::PropertyValue::CreateString(startingDirectory)); + vs.Insert(L"startingTitle", Windows::Foundation::PropertyValue::CreateString(startingTitle)); + vs.Insert(L"initialRows", Windows::Foundation::PropertyValue::CreateUInt32(rows)); + vs.Insert(L"initialCols", Windows::Foundation::PropertyValue::CreateUInt32(columns)); + vs.Insert(L"guid", Windows::Foundation::PropertyValue::CreateGuid(guid)); + + if (environment) + { + Windows::Foundation::Collections::ValueSet env{}; + for (const auto& [k, v] : environment) + { + env.Insert(k, Windows::Foundation::PropertyValue::CreateString(v)); + } + vs.Insert(L"environment", env); + } + return vs; + } + + void ConptyConnection::Initialize(const Windows::Foundation::Collections::ValueSet& settings) + { + if (settings) + { + // For the record, the following won't crash: + // auto bad = unbox_value_or(settings.TryLookup(L"foo").try_as(), nullptr); + // It'll just return null + + _commandline = winrt::unbox_value_or(settings.TryLookup(L"commandline").try_as(), _commandline); + _startingDirectory = winrt::unbox_value_or(settings.TryLookup(L"startingDirectory").try_as(), _startingDirectory); + _startingTitle = winrt::unbox_value_or(settings.TryLookup(L"startingTitle").try_as(), _startingTitle); + _initialRows = winrt::unbox_value_or(settings.TryLookup(L"initialRows").try_as(), _initialRows); + _initialCols = winrt::unbox_value_or(settings.TryLookup(L"initialCols").try_as(), _initialCols); + _guid = winrt::unbox_value_or(settings.TryLookup(L"guid").try_as(), _guid); + _environment = settings.TryLookup(L"environment").try_as(); + } + if (_guid == guid{}) { _guid = Utils::CreateGuid(); @@ -253,12 +356,21 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { _transitionToState(ConnectionState::Connecting); + const COORD dimensions{ gsl::narrow_cast(_initialCols), gsl::narrow_cast(_initialRows) }; + + // If we do not have pipes already, then this is a fresh connection... not an inbound one that is a received + // handoff from an already-started PTY process. if (!_inPipe) { - const COORD dimensions{ gsl::narrow_cast(_initialCols), gsl::narrow_cast(_initialRows) }; THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE, &_inPipe, &_outPipe, &_hPC)); THROW_IF_FAILED(_LaunchAttachedClient()); } + // But if it was an inbound handoff... attempt to synchronize the size of it with what our connection + // window is expecting it to be on the first layout. + else + { + THROW_IF_FAILED(ConptyResizePseudoConsole(_hPC.get(), dimensions)); + } _startTime = std::chrono::high_resolution_clock::now(); @@ -387,11 +499,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void ConptyConnection::Resize(uint32_t rows, uint32_t columns) { - if (!_hPC) + // If we haven't started connecting at all, it's still fair to update + // the initial rows and columns before we set things up. + if (!_isStateAtOrBeyond(ConnectionState::Connecting)) { _initialRows = rows; _initialCols = columns; } + // Otherwise, we can really only dispatch a resize if we're already connected. else if (_isConnected()) { THROW_IF_FAILED(ConptyResizePseudoConsole(_hPC.get(), { Utils::ClampToShortMax(columns, 1), Utils::ClampToShortMax(rows, 1) })); diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index f53b4a56e..8f82a617b 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -26,14 +26,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const HANDLE hServerProcess, const HANDLE hClientProcess); - ConptyConnection( - const hstring& cmdline, - const hstring& startingDirectory, - const hstring& startingTitle, - const Windows::Foundation::Collections::IMapView& environment, - const uint32_t rows, - const uint32_t cols, - const guid& guid); + ConptyConnection() noexcept = default; + void Initialize(const Windows::Foundation::Collections::ValueSet& settings); + static winrt::fire_and_forget final_release(std::unique_ptr connection); void Start(); @@ -49,6 +44,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation static winrt::event_token NewConnection(NewConnectionHandler const& handler); static void NewConnection(winrt::event_token const& token); + static Windows::Foundation::Collections::ValueSet CreateSettings(const winrt::hstring& cmdline, + const winrt::hstring& startingDirectory, + const winrt::hstring& startingTitle, + Windows::Foundation::Collections::IMapView const& environment, + uint32_t rows, + uint32_t columns, + winrt::guid const& guid); + WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler); private: @@ -60,10 +63,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation uint32_t _initialRows{}; uint32_t _initialCols{}; - hstring _commandline; - hstring _startingDirectory; - hstring _startingTitle; - Windows::Foundation::Collections::IMapView _environment; + hstring _commandline{}; + hstring _startingDirectory{}; + hstring _startingTitle{}; + Windows::Foundation::Collections::ValueSet _environment{ nullptr }; guid _guid{}; // A unique session identifier for connected client hstring _clientName{}; // The name of the process hosted by this ConPTY connection (as of launch). @@ -77,9 +80,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation wil::unique_static_pseudoconsole_handle _hPC; wil::unique_threadpool_wait _clientExitWait; - til::u8state _u8State; - std::wstring _u16Str; - std::array _buffer; + til::u8state _u8State{}; + std::wstring _u16Str{}; + std::array _buffer{}; DWORD _OutputThread(); }; diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl index 766c7263a..a1cfa9790 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.idl +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -7,11 +7,19 @@ namespace Microsoft.Terminal.TerminalConnection { [default_interface] runtimeclass ConptyConnection : ITerminalConnection { - ConptyConnection(String cmdline, String startingDirectory, String startingTitle, IMapView environment, UInt32 rows, UInt32 columns, Guid guid); + ConptyConnection(); Guid Guid { get; }; static event NewConnectionHandler NewConnection; static void StartInboundListener(); static void StopInboundListener(); + + static Windows.Foundation.Collections.ValueSet CreateSettings(String cmdline, + String startingDirectory, + String startingTitle, + IMapView environment, + UInt32 rows, + UInt32 columns, + Guid guid); }; } diff --git a/src/cascadia/TerminalConnection/EchoConnection.h b/src/cascadia/TerminalConnection/EchoConnection.h index 0f034457d..e29f07429 100644 --- a/src/cascadia/TerminalConnection/EchoConnection.h +++ b/src/cascadia/TerminalConnection/EchoConnection.h @@ -18,6 +18,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void Resize(uint32_t rows, uint32_t columns) noexcept; void Close() noexcept; + void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) const noexcept {}; + ConnectionState State() const noexcept { return ConnectionState::Connected; } WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler); diff --git a/src/cascadia/TerminalConnection/ITerminalConnection.idl b/src/cascadia/TerminalConnection/ITerminalConnection.idl index e485cc06d..28de4f520 100644 --- a/src/cascadia/TerminalConnection/ITerminalConnection.idl +++ b/src/cascadia/TerminalConnection/ITerminalConnection.idl @@ -17,6 +17,8 @@ namespace Microsoft.Terminal.TerminalConnection interface ITerminalConnection { + void Initialize(Windows.Foundation.Collections.ValueSet settings); + void Start(); void WriteInput(String data); void Resize(UInt32 rows, UInt32 columns); diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index 8af1cf09a..5cfd57c40 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -13,6 +13,9 @@ + + ConnectionInformation.idl + AzureConnection.idl @@ -28,6 +31,9 @@ + + ConnectionInformation.idl + AzureConnection.idl @@ -43,6 +49,7 @@ + diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 932042a8b..d498bd690 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -23,6 +23,16 @@ using namespace winrt::Windows::Graphics::Display; using namespace winrt::Windows::System; using namespace winrt::Windows::ApplicationModel::DataTransfer; +// The minimum delay between updates to the scroll bar's values. +// The updates are throttled to limit power usage. +constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8); + +// The minimum delay between updating the TSF input control. +constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); + +// The minimum delay between updating the locations of regex patterns +constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); + namespace winrt::Microsoft::Terminal::Control::implementation { // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow. @@ -94,6 +104,87 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnTerminalTaskbarProgressChanged = std::bind(&ControlCore::_terminalTaskbarProgressChanged, this); _terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged); + // MSFT 33353327: Initialize the renderer in the ctor instead of Initialize(). + // We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go. + // If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach + // the UIA Engine to the renderer. This prevents us from signaling changes to the cursor or buffer. + { + // First create the render thread. + // Then stash a local pointer to the render thread so we can initialize it and enable it + // to paint itself *after* we hand off its ownership to the renderer. + // We split up construction and initialization of the render thread object this way + // because the renderer and render thread have circular references to each other. + auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); + auto* const localPointerToThread = renderThread.get(); + + // Now create the renderer and initialize the render thread. + _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread)); + + _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { + if (auto strongThis{ weakThis.get() }) + { + strongThis->_RendererEnteredErrorStateHandlers(*strongThis, nullptr); + } + }); + + THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); + } + + // Get our dispatcher. If we're hosted in-proc with XAML, this will get + // us the same dispatcher as TermControl::Dispatcher(). If we're out of + // proc, this'll return null. We'll need to instead make a new + // DispatcherQueue (on a new thread), so we can use that for throttled + // functions. + _dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + if (!_dispatcher) + { + auto controller{ winrt::Windows::System::DispatcherQueueController::CreateOnDedicatedThread() }; + _dispatcher = controller.DispatcherQueue(); + } + + // A few different events should be throttled, so they don't fire absolutely all the time: + // * _tsfTryRedrawCanvas: When the cursor position moves, we need to + // inform TSF, so it can move the canvas for the composition. We + // throttle this so that we're not hopping across the process boundary + // every time that the cursor moves. + // * _updatePatternLocations: When there's new output, or we scroll the + // viewport, we should re-check if there are any visible hyperlinks. + // But we don't really need to do this every single time text is + // output, we can limit this update to once every 500ms. + // * _updateScrollBar: Same idea as the TSF update - we don't _really_ + // need to hop across the process boundary every time text is output. + // We can throttle this to once every 8ms, which will get us out of + // the way of the main output & rendering threads. + _tsfTryRedrawCanvas = std::make_shared>( + _dispatcher, + TsfRedrawInterval, + [weakThis = get_weak()]() { + if (auto core{ weakThis.get() }; !core->_IsClosing()) + { + core->_CursorPositionChangedHandlers(*core, nullptr); + } + }); + + _updatePatternLocations = std::make_shared>( + _dispatcher, + UpdatePatternLocationsInterval, + [weakThis = get_weak()]() { + if (auto core{ weakThis.get() }; !core->_IsClosing()) + { + core->UpdatePatternLocations(); + } + }); + + _updateScrollBar = std::make_shared>( + _dispatcher, + ScrollBarUpdateInterval, + [weakThis = get_weak()](const auto& update) { + if (auto core{ weakThis.get() }; !core->_IsClosing()) + { + core->_ScrollPositionChangedHandlers(*core, update); + } + }); + UpdateSettings(settings); } @@ -131,30 +222,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation return false; } - // First create the render thread. - // Then stash a local pointer to the render thread so we can initialize it and enable it - // to paint itself *after* we hand off its ownership to the renderer. - // We split up construction and initialization of the render thread object this way - // because the renderer and render thread have circular references to each other. - auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); - auto* const localPointerToThread = renderThread.get(); - - // Now create the renderer and initialize the render thread. - _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread)); - ::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer; - - _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { - if (auto strongThis{ weakThis.get() }) - { - strongThis->_RendererEnteredErrorStateHandlers(*strongThis, nullptr); - } - }); - - THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); - // Set up the DX Engine auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); + _renderEngine = std::move(dxEngine); // Initialize our font with the renderer // We don't have to care about DPI. We'll get a change message immediately if it's not 96 @@ -168,12 +239,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Then, using the font, get the number of characters that can fit. // Resize our terminal connection to match that size, and initialize the terminal with that size. const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); - LOG_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); + LOG_IF_FAILED(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); // Update DxEngine's SelectionBackground - dxEngine->SetSelectionBackground(til::color{ _settings.SelectionBackground() }); + _renderEngine->SetSelectionBackground(til::color{ _settings.SelectionBackground() }); - const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); + const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); const auto width = vp.Width(); const auto height = vp.Height(); _connection.Resize(height, width); @@ -182,33 +253,33 @@ namespace winrt::Microsoft::Terminal::Control::implementation _settings.InitialCols(width); _settings.InitialRows(height); - _terminal->CreateFromSettings(_settings, renderTarget); + _terminal->CreateFromSettings(_settings, *_renderer); // IMPORTANT! Set this callback up sooner than later. If we do it // after Enable, then it'll be possible to paint the frame once // _before_ the warning handler is set up, and then warnings from // the first paint will be ignored! - dxEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); + _renderEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); // Tell the DX Engine to notify us when the swap chain changes. // We do this after we initially set the swapchain so as to avoid unnecessary callbacks (and locking problems) - dxEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); + _renderEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); - dxEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); - dxEngine->SetPixelShaderPath(_settings.PixelShaderPath()); - dxEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); - dxEngine->SetSoftwareRendering(_settings.SoftwareRendering()); + _renderEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); + _renderEngine->SetPixelShaderPath(_settings.PixelShaderPath()); + _renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); + _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); + _renderEngine->SetIntenseIsBold(_settings.IntenseIsBold()); - _updateAntiAliasingMode(dxEngine.get()); + _updateAntiAliasingMode(_renderEngine.get()); // GH#5098: Inform the engine of the opacity of the default text background. if (_settings.UseAcrylic()) { - dxEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); + _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); } - THROW_IF_FAILED(dxEngine->Enable()); - _renderEngine = std::move(dxEngine); + THROW_IF_FAILED(_renderEngine->Enable()); _initializedTerminal = true; } // scope for TerminalLock @@ -426,7 +497,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - Updates last hovered cell, renders / removes rendering of hyper-link if required // Arguments: // - terminalPosition: The terminal position of the pointer - void ControlCore::UpdateHoveredCell(const std::optional& terminalPosition) + void ControlCore::SetHoveredCell(Core::Point pos) + { + _updateHoveredCell(std::optional{ pos }); + } + void ControlCore::ClearHoveredCell() + { + _updateHoveredCell(std::nullopt); + } + + void ControlCore::_updateHoveredCell(const std::optional terminalPosition) { if (terminalPosition == _lastHoveredCell) { @@ -477,7 +557,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return winrt::hstring{ _terminal->GetHyperlinkAtPosition(pos) }; } - winrt::hstring ControlCore::GetHoveredUriText() const + winrt::hstring ControlCore::HoveredUriText() const { auto lock = _terminal->LockForReading(); // Lock for the duration of our reads. if (_lastHoveredCell.has_value()) @@ -487,9 +567,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation return {}; } - std::optional ControlCore::GetHoveredCell() const + Windows::Foundation::IReference ControlCore::HoveredCell() const { - return _lastHoveredCell; + return _lastHoveredCell.has_value() ? Windows::Foundation::IReference{ _lastHoveredCell.value() } : nullptr; } // Method Description: @@ -526,6 +606,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); + _updateAntiAliasingMode(_renderEngine.get()); // Refresh our font with the renderer @@ -555,6 +636,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderEngine->SetSelectionBackground(til::color{ newAppearance.SelectionBackground() }); _renderEngine->SetRetroTerminalEffect(newAppearance.RetroTerminalEffect()); _renderEngine->SetPixelShaderPath(newAppearance.PixelShaderPath()); + _renderEngine->SetIntenseIsBold(_settings.IntenseIsBold()); _renderer->TriggerRedrawAll(); } } @@ -594,9 +676,34 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetFontInfo(_actualFont); - // TODO: MSFT:20895307 If the font doesn't exist, this doesn't - // actually fail. We need a way to gracefully fallback. - _renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont); + if (_renderEngine) + { + std::unordered_map featureMap; + if (const auto fontFeatures = _settings.FontFeatures()) + { + featureMap.reserve(fontFeatures.Size()); + + for (const auto& [tag, param] : fontFeatures) + { + featureMap.emplace(tag, param); + } + } + std::unordered_map axesMap; + if (const auto fontAxes = _settings.FontAxes()) + { + axesMap.reserve(fontAxes.Size()); + + for (const auto& [axis, value] : fontAxes) + { + axesMap.emplace(axis, value); + } + } + + // TODO: MSFT:20895307 If the font doesn't exist, this doesn't + // actually fail. We need a way to gracefully fallback. + LOG_IF_FAILED(_renderEngine->UpdateDpi(newDpi)); + LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap)); + } // If the actual font isn't what was requested... if (_actualFont.GetFaceName() != _desiredFont.GetFaceName()) @@ -705,6 +812,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } + // Convert our new dimensions to characters + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, + { static_cast(size.cx), static_cast(size.cy) }); + const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); + const auto currentVP = _terminal->GetViewport(); + + // Don't actually resize if viewport dimensions didn't change + if (vp.Height() == currentVP.Height() && vp.Width() == currentVP.Width()) + { + return; + } + _terminal->ClearSelection(); // Tell the dx engine that our window is now the new size. @@ -713,11 +832,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Invalidate everything _renderer->TriggerRedrawAll(); - // Convert our new dimensions to characters - const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, - { static_cast(size.cx), static_cast(size.cy) }); - const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); - // If this function succeeds with S_FALSE, then the terminal didn't // actually change size. No need to notify the connection of this no-op. const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); @@ -895,6 +1009,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _actualFont; } + winrt::Windows::Foundation::Size ControlCore::FontSize() const noexcept + { + const auto fontSize = GetFont().GetSize(); + return { + ::base::saturated_cast(fontSize.X), + ::base::saturated_cast(fontSize.Y) + }; + } + winrt::hstring ControlCore::FontFaceName() const noexcept + { + return winrt::hstring{ GetFont().GetFaceName() }; + } + + uint16_t ControlCore::FontWeight() const noexcept + { + return static_cast(GetFont().GetWeight()); + } + til::size ControlCore::FontSizeInDips() const { const til::size fontSize{ GetFont().GetSize() }; @@ -1051,15 +1183,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation // TODO GH#9617: refine locking around pattern tree _terminal->ClearPatternTree(); - _ScrollPositionChangedHandlers(*this, - winrt::make(viewTop, - viewHeight, - bufferSize)); + // Start the throttled update of our scrollbar. + auto update{ winrt::make(viewTop, + viewHeight, + bufferSize) }; + if (!_inUnitTests) + { + _updateScrollBar->Run(update); + } + else + { + _ScrollPositionChangedHandlers(*this, update); + } + + // Additionally, start the throttled update of where our links are. + _updatePatternLocations->Run(); } void ControlCore::_terminalCursorPositionChanged() { - _CursorPositionChangedHandlers(*this, nullptr); + // When the buffer's cursor moves, start the throttled func to + // eventually dispatch a CursorPositionChanged event. + _tsfTryRedrawCanvas->Run(); } void ControlCore::_terminalTaskbarProgressChanged() @@ -1077,10 +1222,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _settings.CopyOnSelect(); } - std::vector ControlCore::SelectedText(bool trimTrailingWhitespace) const + Windows::Foundation::Collections::IVector ControlCore::SelectedText(bool trimTrailingWhitespace) const { // RetrieveSelectedTextFromBuffer will lock while it's reading - return _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text; + const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text }; + + auto result = winrt::single_threaded_vector(); + + for (const auto& row : internalResult) + { + result.Append(winrt::hstring{ row }); + } + return result; } ::Microsoft::Console::Types::IUiaData* ControlCore::GetUiaData() const @@ -1124,7 +1277,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - void ControlCore::SetBackgroundOpacity(const float opacity) + void ControlCore::SetBackgroundOpacity(const double opacity) { if (_renderEngine) { @@ -1161,8 +1314,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::Close() { - if (!_closing.exchange(true)) + if (!_IsClosing()) { + _closing = true; + // Stop accepting new output and state changes before we disconnect everything. _connection.TerminalOutput(_connectionOutputEventToken); _connectionStateChangedRevoker.revoke(); @@ -1176,7 +1331,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - HANDLE ControlCore::GetSwapChainHandle() const + uint64_t ControlCore::SwapChainHandle() const { // This is called by: // * TermControl::RenderEngineSwapChainChanged, who is only registered @@ -1184,7 +1339,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // * TermControl::_InitializeTerminal, after the call to Initialize, for // _AttachDxgiSwapChainToXaml. // In both cases, we'll have a _renderEngine by then. - return _renderEngine->GetSwapChainHandle(); + return reinterpret_cast(_renderEngine->GetSwapChainHandle()); } void ControlCore::_rendererWarning(const HRESULT hr) @@ -1315,10 +1470,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine) { - if (_renderer) - { - _renderer->AddRenderEngine(pEngine); - } + // _renderer will always exist since it's introduced in the ctor + _renderer->AddRenderEngine(pEngine); } bool ControlCore::IsInReadOnlyMode() const @@ -1340,18 +1493,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _terminal->Write(hstr); - // NOTE: We're raising an event here to inform the TermControl that - // output has been received, so it can queue up a throttled - // UpdatePatternLocations call. In the future, we should have the - // _updatePatternLocations ThrottledFunc internal to this class, and - // run on this object's dispatcher queue. - // - // We're not doing that quite yet, because the Core will eventually - // be out-of-proc from the UI thread, and won't be able to just use - // the UI thread as the dispatcher queue thread. - // - // See TODO: https://github.com/microsoft/terminal/projects/5#card-50760282 - _ReceivedOutputHandlers(*this, nullptr); + // Start the throttled update of where our hyperlinks are. + _updatePatternLocations->Run(); } } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index e4941e00a..3bb321f0b 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -48,15 +48,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation void UpdateAppearance(const IControlAppearance& newAppearance); void SizeChanged(const double width, const double height); void ScaleChanged(const double scale); - HANDLE GetSwapChainHandle() const; + uint64_t SwapChainHandle() const; void AdjustFontSize(int fontSizeDelta); void ResetFontSize(); FontInfo GetFont() const; til::size FontSizeInDips() const; + winrt::Windows::Foundation::Size FontSize() const noexcept; + winrt::hstring FontFaceName() const noexcept; + uint16_t FontWeight() const noexcept; + til::color BackgroundColor() const; - void SetBackgroundOpacity(const float opacity); + void SetBackgroundOpacity(const double opacity); void SendInput(const winrt::hstring& wstr); void PasteText(const winrt::hstring& hstr); @@ -67,10 +71,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ResumeRendering(); void UpdatePatternLocations(); - void UpdateHoveredCell(const std::optional& terminalPosition); + void SetHoveredCell(Core::Point terminalPosition); + void ClearHoveredCell(); winrt::hstring GetHyperlink(const til::point position) const; - winrt::hstring GetHoveredUriText() const; - std::optional GetHoveredCell() const; + winrt::hstring HoveredUriText() const; + Windows::Foundation::IReference HoveredCell() const; ::Microsoft::Console::Types::IUiaData* GetUiaData() const; @@ -119,7 +124,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool HasSelection() const; bool CopyOnSelect() const; - std::vector SelectedText(bool trimTrailingWhitespace) const; + Windows::Foundation::Collections::IVector SelectedText(bool trimTrailingWhitespace) const; void SetSelectionAnchor(til::point const& position); void SetEndSelectionPoint(til::point const& position); @@ -163,7 +168,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation private: bool _initializedTerminal{ false }; - std::atomic _closing{ false }; + bool _closing{ false }; TerminalConnection::ITerminalConnection _connection{ nullptr }; event_token _connectionOutputEventToken; @@ -201,6 +206,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation double _panelHeight{ 0 }; double _compositionScale{ 0 }; + winrt::Windows::System::DispatcherQueue _dispatcher{ nullptr }; + std::shared_ptr> _tsfTryRedrawCanvas; + std::shared_ptr> _updatePatternLocations; + std::shared_ptr> _updateScrollBar; + winrt::fire_and_forget _asyncCloseConnection(); void _setFontSize(int fontSize); @@ -232,9 +242,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _raiseReadOnlyWarning(); void _updateAntiAliasingMode(::Microsoft::Console::Render::DxEngine* const dxEngine); void _connectionOutputHandler(const hstring& hstr); + void _updateHoveredCell(const std::optional terminalPosition); + + inline bool _IsClosing() const noexcept + { +#ifndef NDEBUG + if (_dispatcher) + { + // _closing isn't atomic and may only be accessed from the main thread. + // + // Though, the unit tests don't actually run in TAEF's main + // thread, so we don't care when we're running in tests. + assert(_inUnitTests || _dispatcher.HasThreadAccess()); + } +#endif + return _closing; + } friend class ControlUnitTests::ControlCoreTests; friend class ControlUnitTests::ControlInteractivityTests; + bool _inUnitTests{ false }; }; } diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index c3074000f..fa4746ca4 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -8,9 +8,97 @@ import "EventArgs.idl"; namespace Microsoft.Terminal.Control { + // This is a mirror of + // ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState, + // but projectable. + // !! LOAD BEARING !! If you make this a struct with Booleans (like they + // make the most sense as), then the app will crash trying to toss one of + // these across the process boundary. I haven't the damndest idea why. + [flags] + enum MouseButtonState + { + IsLeftButtonDown = 0x1, + IsMiddleButtonDown = 0x2, + IsRightButtonDown = 0x4 + }; + [default_interface] runtimeclass ControlCore : ICoreState { ControlCore(IControlSettings settings, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); + + Boolean Initialize(Double actualWidth, + Double actualHeight, + Double compositionScale); + + void UpdateSettings(IControlSettings settings); + void UpdateAppearance(IControlAppearance appearance); + + UInt64 SwapChainHandle { get; }; + + Windows.Foundation.Size FontSize { get; }; + String FontFaceName { get; }; + UInt16 FontWeight { get; }; + + Boolean TrySendKeyEvent(Int16 vkey, + Int16 scanCode, + Microsoft.Terminal.Core.ControlKeyStates modifiers, + Boolean keyDown); + Boolean SendCharEvent(Char ch, + Int16 scanCode, + Microsoft.Terminal.Core.ControlKeyStates modifiers); + void SendInput(String text); + void PasteText(String text); + + void SetHoveredCell(Microsoft.Terminal.Core.Point terminalPosition); + void ClearHoveredCell(); + + void ResetFontSize(); + void AdjustFontSize(Int32 fontSizeDelta); + void SizeChanged(Double width, Double height); + void ScaleChanged(Double scale); + + void ToggleShaderEffects(); + void ToggleReadOnlyMode(); + + Microsoft.Terminal.Core.Point CursorPosition { get; }; + void ResumeRendering(); + void BlinkAttributeTick(); + void UpdatePatternLocations(); + void Search(String text, Boolean goForward, Boolean caseSensitive); + void SetBackgroundOpacity(Double opacity); + Microsoft.Terminal.Core.Color BackgroundColor { get; }; + + Boolean HasSelection { get; }; + IVector SelectedText(Boolean trimTrailingWhitespace); + + String HoveredUriText { get; }; + Windows.Foundation.IReference HoveredCell { get; }; + + void Close(); + void BlinkCursor(); + Boolean IsInReadOnlyMode { get; }; + Boolean CursorOn; + void EnablePainting(); + + event FontSizeChangedEventArgs FontSizeChanged; + + event Windows.Foundation.TypedEventHandler CopyToClipboard; + event Windows.Foundation.TypedEventHandler TitleChanged; + event Windows.Foundation.TypedEventHandler WarningBell; + event Windows.Foundation.TypedEventHandler TabColorChanged; + event Windows.Foundation.TypedEventHandler BackgroundColorChanged; + event Windows.Foundation.TypedEventHandler ScrollPositionChanged; + event Windows.Foundation.TypedEventHandler CursorPositionChanged; + event Windows.Foundation.TypedEventHandler TaskbarProgressChanged; + event Windows.Foundation.TypedEventHandler ConnectionStateChanged; + event Windows.Foundation.TypedEventHandler HoveredHyperlinkChanged; + event Windows.Foundation.TypedEventHandler RendererEnteredErrorState; + event Windows.Foundation.TypedEventHandler SwapChainChanged; + event Windows.Foundation.TypedEventHandler RendererWarning; + event Windows.Foundation.TypedEventHandler RaiseNotice; + event Windows.Foundation.TypedEventHandler TransparencyChanged; + event Windows.Foundation.TypedEventHandler ReceivedOutput; + }; } diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 433150cab..55d389869 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -8,13 +8,15 @@ #include #include #include -#include #include #include "../../types/inc/GlyphWidth.hpp" #include "../../types/inc/Utils.hpp" #include "../../buffer/out/search.h" +#include "InteractivityAutomationPeer.h" + #include "ControlInteractivity.g.cpp" +#include "TermControl.h" using namespace ::Microsoft::Console::Types; using namespace ::Microsoft::Console::VirtualTerminal; @@ -27,6 +29,15 @@ static constexpr unsigned int MAX_CLICK_COUNT = 3; namespace winrt::Microsoft::Terminal::Control::implementation { + static constexpr TerminalInput::MouseButtonState toInternalMouseState(const Control::MouseButtonState& state) + { + return TerminalInput::MouseButtonState{ + WI_IsFlagSet(state, MouseButtonState::IsLeftButtonDown), + WI_IsFlagSet(state, MouseButtonState::IsMiddleButtonDown), + WI_IsFlagSet(state, MouseButtonState::IsRightButtonDown) + }; + } + ControlInteractivity::ControlInteractivity(IControlSettings settings, TerminalConnection::ITerminalConnection connection) : _touchAnchor{ std::nullopt }, @@ -37,6 +48,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core = winrt::make_self(settings, connection); } + // Method Description: + // - Updates our internal settings. These settings should be + // interactivity-specific. Right now, we primarily update _rowsToScroll + // with the current value of SPI_GETWHEELSCROLLLINES. + // Arguments: + // - + // Return Value: + // - void ControlInteractivity::UpdateSettings() { _updateSystemParameterSettings(); @@ -50,9 +69,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation _multiClickTimer = GetDoubleClickTime() * 1000; } - winrt::com_ptr ControlInteractivity::GetCore() + Control::ControlCore ControlInteractivity::Core() { - return _core; + return *_core; } // Method Description: @@ -85,11 +104,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _multiClickCounter; } - void ControlInteractivity::GainFocus() + void ControlInteractivity::GotFocus() { + if (_uiaEngine.get()) + { + THROW_IF_FAILED(_uiaEngine->Enable()); + } + _updateSystemParameterSettings(); } + void ControlInteractivity::LostFocus() + { + if (_uiaEngine.get()) + { + THROW_IF_FAILED(_uiaEngine->Disable()); + } + } + // Method Description // - Updates internal params based on system parameters void ControlInteractivity::_updateSystemParameterSettings() noexcept @@ -156,7 +188,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core->PasteText(winrt::hstring{ wstr }); } - void ControlInteractivity::PointerPressed(TerminalInput::MouseButtonState buttonState, + void ControlInteractivity::PointerPressed(Control::MouseButtonState buttonState, const unsigned int pointerUpdateKind, const uint64_t timestamp, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, @@ -170,7 +202,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // GH#9396: we prioritize hyper-link over VT mouse events auto hyperlink = _core->GetHyperlink(terminalPosition); - if (buttonState.isLeftButtonDown && + if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown) && ctrlEnabled && !hyperlink.empty()) { const auto clickCount = _numberOfClicks(pixelPosition, timestamp); @@ -182,9 +214,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (_canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); + const auto adjustment = _core->ScrollOffset() > 0 ? _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight() : 0; + // If the click happened outside the active region, just don't send any mouse event + if (const auto adjustedY = terminalPosition.y() - adjustment; adjustedY >= 0) + { + _core->SendMouseEvent({ terminalPosition.x(), adjustedY }, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); + } } - else if (buttonState.isLeftButtonDown) + else if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)) { const auto clickCount = _numberOfClicks(pixelPosition, timestamp); // This formula enables the number of clicks to cycle properly @@ -219,7 +256,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _singleClickTouchdownPos = std::nullopt; } } - else if (buttonState.isRightButtonDown) + else if (WI_IsFlagSet(buttonState, MouseButtonState::IsRightButtonDown)) { // CopyOnSelect right click always pastes if (_core->CopyOnSelect() || !_core->HasSelection()) @@ -238,20 +275,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation _touchAnchor = contactPoint; } - void ControlInteractivity::PointerMoved(TerminalInput::MouseButtonState buttonState, + void ControlInteractivity::PointerMoved(Control::MouseButtonState buttonState, const unsigned int pointerUpdateKind, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool focused, - const til::point pixelPosition) + const til::point pixelPosition, + const bool pointerPressedInBounds) { const til::point terminalPosition = _getTerminalPosition(pixelPosition); // Short-circuit isReadOnly check to avoid warning dialog if (focused && !_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); + _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); } - else if (focused && buttonState.isLeftButtonDown) + // GH#4603 - don't modify the selection if the pointer press didn't + // actually start _in_ the control bounds. Case in point - someone drags + // a file into the bounds of the control. That shouldn't send the + // selection into space. + else if (focused && pointerPressedInBounds && WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)) { if (_singleClickTouchdownPos) { @@ -279,7 +321,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation SetEndSelectionPoint(pixelPosition); } - _core->UpdateHoveredCell(terminalPosition); + _core->SetHoveredCell(terminalPosition); } void ControlInteractivity::TouchMoved(const til::point newTouchPoint, @@ -319,7 +361,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - void ControlInteractivity::PointerReleased(TerminalInput::MouseButtonState buttonState, + void ControlInteractivity::PointerReleased(Control::MouseButtonState buttonState, const unsigned int pointerUpdateKind, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const til::point pixelPosition) @@ -328,7 +370,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Short-circuit isReadOnly check to avoid warning dialog if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); + _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); return; } @@ -366,7 +408,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ControlInteractivity::MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const int32_t delta, const til::point pixelPosition, - const TerminalInput::MouseButtonState state) + const Control::MouseButtonState buttonState) { const til::point terminalPosition = _getTerminalPosition(pixelPosition); @@ -382,7 +424,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation WM_MOUSEWHEEL, modifiers, ::base::saturated_cast(delta), - state); + toInternalMouseState(buttonState)); } const auto ctrlPressed = modifiers.IsCtrlPressed(); @@ -398,7 +440,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else { - _mouseScrollHandler(delta, pixelPosition, state.isLeftButtonDown); + _mouseScrollHandler(delta, pixelPosition, WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)); } return false; } @@ -557,4 +599,36 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Convert the location in pixels to characters within the current viewport. return til::point{ pixelPosition / fontSize }; } + + // Method Description: + // - Creates an automation peer for the Terminal Control, enabling + // accessibility on our control. + // - Our implementation implements the ITextProvider pattern, and the + // IControlAccessibilityInfo, to connect to the UiaEngine, which must be + // attached to the core's renderer. + // - The TermControlAutomationPeer will connect this to the UI tree. + // Arguments: + // - None + // Return Value: + // - The automation peer for our control + Control::InteractivityAutomationPeer ControlInteractivity::OnCreateAutomationPeer() + try + { + const auto autoPeer = winrt::make_self(this); + + _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get()); + _core->AttachUiaEngine(_uiaEngine.get()); + return *autoPeer; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + return nullptr; + } + + ::Microsoft::Console::Types::IUiaData* ControlInteractivity::GetUiaData() const + { + return _core->GetUiaData(); + } + } diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index f684f2dff..937534838 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -23,11 +23,6 @@ #include "ControlCore.h" -namespace Microsoft::Console::VirtualTerminal -{ - struct MouseButtonState; -} - namespace ControlUnitTests { class ControlCoreTests; @@ -42,28 +37,33 @@ namespace winrt::Microsoft::Terminal::Control::implementation ControlInteractivity(IControlSettings settings, TerminalConnection::ITerminalConnection connection); - void GainFocus(); + void GotFocus(); + void LostFocus(); void UpdateSettings(); void Initialize(); - winrt::com_ptr GetCore(); + Control::ControlCore Core(); + + Control::InteractivityAutomationPeer OnCreateAutomationPeer(); + ::Microsoft::Console::Types::IUiaData* GetUiaData() const; #pragma region Input Methods - void PointerPressed(::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState buttonState, + void PointerPressed(Control::MouseButtonState buttonState, const unsigned int pointerUpdateKind, const uint64_t timestamp, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const til::point pixelPosition); void TouchPressed(const til::point contactPoint); - void PointerMoved(::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState buttonState, + void PointerMoved(Control::MouseButtonState buttonState, const unsigned int pointerUpdateKind, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool focused, - const til::point pixelPosition); + const til::point pixelPosition, + const bool pointerPressedInBounds); void TouchMoved(const til::point newTouchPoint, const bool focused); - void PointerReleased(::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState buttonState, + void PointerReleased(Control::MouseButtonState buttonState, const unsigned int pointerUpdateKind, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const til::point pixelPosition); @@ -72,7 +72,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const int32_t delta, const til::point pixelPosition, - const ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state); + const Control::MouseButtonState state); void UpdateScrollbar(const double newValue); @@ -83,7 +83,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation void RequestPasteTextFromClipboard(); void SetEndSelectionPoint(const til::point pixelPosition); + TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); + TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); + TYPED_EVENT(ScrollPositionChanged, IInspectable, Control::ScrollPositionChangedArgs); + private: + // NOTE: _uiaEngine must be ordered before _core. + // + // ControlCore::AttachUiaEngine receives a IRenderEngine as a raw pointer, which we own. + // We must ensure that we first destroy the ControlCore before the UiaEngine instance + // in order to safely resolve this unsafe pointer dependency. Otherwise a deallocated + // IRenderEngine is accessed when ControlCore calls Renderer::TriggerTeardown. + // (C++ class members are destroyed in reverse order.) + std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine; + winrt::com_ptr _core{ nullptr }; unsigned int _rowsToScroll; double _internalScrollbarPosition{ 0.0 }; @@ -129,10 +142,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _sendPastedTextToConnection(std::wstring_view wstr); til::point _getTerminalPosition(const til::point& pixelPosition); - TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); - TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); - TYPED_EVENT(ScrollPositionChanged, IInspectable, Control::ScrollPositionChangedArgs); - friend class ControlUnitTests::ControlCoreTests; friend class ControlUnitTests::ControlInteractivityTests; }; diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl index 3a9afaa14..6261b773d 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.idl +++ b/src/cascadia/TerminalControl/ControlInteractivity.idl @@ -5,6 +5,8 @@ import "ICoreState.idl"; import "IControlSettings.idl"; import "ControlCore.idl"; import "EventArgs.idl"; +import "InteractivityAutomationPeer.idl"; + namespace Microsoft.Terminal.Control { @@ -13,5 +15,53 @@ namespace Microsoft.Terminal.Control { ControlInteractivity(IControlSettings settings, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); + + ControlCore Core { get; }; + void UpdateSettings(); + void Initialize(); + void GotFocus(); + void LostFocus(); + + InteractivityAutomationPeer OnCreateAutomationPeer(); + + Boolean CopySelectionToClipboard(Boolean singleLine, Windows.Foundation.IReference formats); + void RequestPasteTextFromClipboard(); + void SetEndSelectionPoint(Microsoft.Terminal.Core.Point point); + + void PointerPressed(MouseButtonState buttonState, + UInt32 pointerUpdateKind, + UInt64 timestamp, + Microsoft.Terminal.Core.ControlKeyStates modifiers, + Microsoft.Terminal.Core.Point pixelPosition); + void TouchPressed(Microsoft.Terminal.Core.Point contactPoint); + + void PointerMoved(MouseButtonState buttonState, + UInt32 pointerUpdateKind, + Microsoft.Terminal.Core.ControlKeyStates modifiers, + Boolean focused, + Microsoft.Terminal.Core.Point pixelPosition, + Boolean pointerPressedInBounds); + + void TouchMoved(Microsoft.Terminal.Core.Point newTouchPoint, + Boolean focused); + + void PointerReleased(MouseButtonState buttonState, + UInt32 pointerUpdateKind, + Microsoft.Terminal.Core.ControlKeyStates modifiers, + Microsoft.Terminal.Core.Point pixelPosition); + void TouchReleased(); + + Boolean MouseWheel(Microsoft.Terminal.Core.ControlKeyStates modifiers, + Int32 delta, + Microsoft.Terminal.Core.Point pixelPosition, + MouseButtonState state); + + void UpdateScrollbar(Double newValue); + + event Windows.Foundation.TypedEventHandler OpenHyperlink; + event Windows.Foundation.TypedEventHandler ScrollPositionChanged; + event Windows.Foundation.TypedEventHandler PasteFromClipboard; + + }; } diff --git a/src/cascadia/TerminalControl/IControlAppearance.idl b/src/cascadia/TerminalControl/IControlAppearance.idl index 88de9e4b6..a0fa0a0ad 100644 --- a/src/cascadia/TerminalControl/IControlAppearance.idl +++ b/src/cascadia/TerminalControl/IControlAppearance.idl @@ -11,6 +11,8 @@ namespace Microsoft.Terminal.Control Windows.UI.Xaml.Media.Stretch BackgroundImageStretchMode; Windows.UI.Xaml.HorizontalAlignment BackgroundImageHorizontalAlignment; Windows.UI.Xaml.VerticalAlignment BackgroundImageVerticalAlignment; + Boolean IntenseIsBold; + // IntenseIsBright is in Core Appearance // Experimental settings Boolean RetroTerminalEffect; diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index 1bbc17809..56aab7395 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -36,6 +36,8 @@ namespace Microsoft.Terminal.Control Int32 FontSize; Windows.UI.Text.FontWeight FontWeight; String Padding; + Windows.Foundation.Collections.IMap FontFeatures; + Windows.Foundation.Collections.IMap FontAxes; Microsoft.Terminal.Control.IKeyBindings KeyBindings; diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl index a21c0747c..ad5217bf6 100644 --- a/src/cascadia/TerminalControl/ICoreState.idl +++ b/src/cascadia/TerminalControl/ICoreState.idl @@ -17,6 +17,7 @@ namespace Microsoft.Terminal.Control Int32 ScrollOffset { get; }; Int32 ViewHeight { get; }; + Int32 BufferHeight { get; }; Boolean BracketedPasteEnabled { get; }; diff --git a/src/cascadia/TerminalControl/IKeyBindings.idl b/src/cascadia/TerminalControl/IKeyBindings.idl index 2b3901559..83d665faa 100644 --- a/src/cascadia/TerminalControl/IKeyBindings.idl +++ b/src/cascadia/TerminalControl/IKeyBindings.idl @@ -9,5 +9,6 @@ namespace Microsoft.Terminal.Control interface IKeyBindings { Boolean TryKeyChord(KeyChord kc); + Boolean IsKeyChordExplicitlyUnbound(KeyChord kc); } } diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp b/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp new file mode 100644 index 000000000..9d028b484 --- /dev/null +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include +#include +#include "InteractivityAutomationPeer.h" +#include "InteractivityAutomationPeer.g.cpp" + +#include "XamlUiaTextRange.h" +#include "../types/UiaTracing.h" + +using namespace Microsoft::Console::Types; +using namespace winrt::Windows::UI::Xaml::Automation::Peers; +using namespace winrt::Windows::Graphics::Display; + +namespace UIA +{ + using ::ITextRangeProvider; + using ::SupportedTextSelection; +} + +namespace XamlAutomation +{ + using winrt::Windows::UI::Xaml::Automation::SupportedTextSelection; + using winrt::Windows::UI::Xaml::Automation::Provider::IRawElementProviderSimple; + using winrt::Windows::UI::Xaml::Automation::Provider::ITextRangeProvider; +} + +namespace winrt::Microsoft::Terminal::Control::implementation +{ + InteractivityAutomationPeer::InteractivityAutomationPeer(Control::implementation::ControlInteractivity* owner) : + _interactivity{ owner } + { + THROW_IF_FAILED(::Microsoft::WRL::MakeAndInitialize<::Microsoft::Terminal::TermControlUiaProvider>(&_uiaProvider, _interactivity->GetUiaData(), this)); + }; + + void InteractivityAutomationPeer::SetControlBounds(const Windows::Foundation::Rect bounds) + { + _controlBounds = til::rectangle{ til::math::rounding, bounds }; + } + void InteractivityAutomationPeer::SetControlPadding(const Core::Padding padding) + { + _controlPadding = padding; + } + + // Method Description: + // - Signals the ui automation client that the terminal's selection has + // changed and should be updated + // - We will raise a new event, for out embedding control to be able to + // raise the event. AutomationPeer by itself doesn't hook up to the + // eventing mechanism, we need the FrameworkAutomationPeer to do that. + // Arguments: + // - + // Return Value: + // - + void InteractivityAutomationPeer::SignalSelectionChanged() + { + _SelectionChangedHandlers(*this, nullptr); + } + + // Method Description: + // - Signals the ui automation client that the terminal's output has changed + // and should be updated + // - We will raise a new event, for out embedding control to be able to + // raise the event. AutomationPeer by itself doesn't hook up to the + // eventing mechanism, we need the FrameworkAutomationPeer to do that. + // Arguments: + // - + // Return Value: + // - + void InteractivityAutomationPeer::SignalTextChanged() + { + _TextChangedHandlers(*this, nullptr); + } + + // Method Description: + // - Signals the ui automation client that the cursor's state has changed + // and should be updated + // - We will raise a new event, for out embedding control to be able to + // raise the event. AutomationPeer by itself doesn't hook up to the + // eventing mechanism, we need the FrameworkAutomationPeer to do that. + // Arguments: + // - + // Return Value: + // - + void InteractivityAutomationPeer::SignalCursorChanged() + { + _CursorChangedHandlers(*this, nullptr); + } + +#pragma region ITextProvider + com_array InteractivityAutomationPeer::GetSelection() + { + SAFEARRAY* pReturnVal; + THROW_IF_FAILED(_uiaProvider->GetSelection(&pReturnVal)); + return WrapArrayOfTextRangeProviders(pReturnVal); + } + + com_array InteractivityAutomationPeer::GetVisibleRanges() + { + SAFEARRAY* pReturnVal; + THROW_IF_FAILED(_uiaProvider->GetVisibleRanges(&pReturnVal)); + return WrapArrayOfTextRangeProviders(pReturnVal); + } + + XamlAutomation::ITextRangeProvider InteractivityAutomationPeer::RangeFromChild(XamlAutomation::IRawElementProviderSimple childElement) + { + UIA::ITextRangeProvider* returnVal; + // ScreenInfoUiaProvider doesn't actually use parameter, so just pass in nullptr + THROW_IF_FAILED(_uiaProvider->RangeFromChild(/* IRawElementProviderSimple */ nullptr, + &returnVal)); + + const auto parentProvider = this->ProviderFromPeer(*this); + const auto xutr = winrt::make_self(returnVal, parentProvider); + return xutr.as(); + } + + XamlAutomation::ITextRangeProvider InteractivityAutomationPeer::RangeFromPoint(Windows::Foundation::Point screenLocation) + { + UIA::ITextRangeProvider* returnVal; + THROW_IF_FAILED(_uiaProvider->RangeFromPoint({ screenLocation.X, screenLocation.Y }, &returnVal)); + + const auto parentProvider = this->ProviderFromPeer(*this); + const auto xutr = winrt::make_self(returnVal, parentProvider); + return xutr.as(); + } + + XamlAutomation::ITextRangeProvider InteractivityAutomationPeer::DocumentRange() + { + UIA::ITextRangeProvider* returnVal; + THROW_IF_FAILED(_uiaProvider->get_DocumentRange(&returnVal)); + + const auto parentProvider = this->ProviderFromPeer(*this); + const auto xutr = winrt::make_self(returnVal, parentProvider); + return xutr.as(); + } + + XamlAutomation::SupportedTextSelection InteractivityAutomationPeer::SupportedTextSelection() + { + UIA::SupportedTextSelection returnVal; + THROW_IF_FAILED(_uiaProvider->get_SupportedTextSelection(&returnVal)); + return static_cast(returnVal); + } + +#pragma endregion + +#pragma region IControlAccessibilityInfo + COORD InteractivityAutomationPeer::GetFontSize() const + { + return til::size{ til::math::rounding, _interactivity->Core().FontSize() }; + } + + RECT InteractivityAutomationPeer::GetBounds() const + { + return _controlBounds; + } + + HRESULT InteractivityAutomationPeer::GetHostUiaProvider(IRawElementProviderSimple** provider) + { + RETURN_HR_IF(E_INVALIDARG, provider == nullptr); + *provider = nullptr; + + return S_OK; + } + + RECT InteractivityAutomationPeer::GetPadding() const + { + return _controlPadding; + } + + double InteractivityAutomationPeer::GetScaleFactor() const + { + return DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); + } + + void InteractivityAutomationPeer::ChangeViewport(const SMALL_RECT NewWindow) + { + _interactivity->UpdateScrollbar(NewWindow.Top); + } +#pragma endregion + + // Method Description: + // - extracts the UiaTextRanges from the SAFEARRAY and converts them to Xaml ITextRangeProviders + // Arguments: + // - SAFEARRAY of UIA::UiaTextRange (ITextRangeProviders) + // Return Value: + // - com_array of Xaml Wrapped UiaTextRange (ITextRangeProviders) + com_array InteractivityAutomationPeer::WrapArrayOfTextRangeProviders(SAFEARRAY* textRanges) + { + // transfer ownership of UiaTextRanges to this new vector + auto providers = SafeArrayToOwningVector<::Microsoft::Terminal::TermControlUiaTextRange>(textRanges); + int count = gsl::narrow(providers.size()); + + std::vector vec; + vec.reserve(count); + auto parentProvider = this->ProviderFromPeer(*this); + for (int i = 0; i < count; i++) + { + auto xutr = make_self(providers[i].detach(), parentProvider); + vec.emplace_back(xutr.as()); + } + + com_array result{ vec }; + + return result; + } +} diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.h b/src/cascadia/TerminalControl/InteractivityAutomationPeer.h new file mode 100644 index 000000000..893478e91 --- /dev/null +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.h @@ -0,0 +1,85 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- InteractivityAutomationPeer.h + +Abstract: +- This module provides UI Automation access to the ControlInteractivity, + to support both automation tests and accessibility (screen + reading) applications. +- See TermControlAutomationPeer for more details on how UIA is implemented. +- This is the primary implementation of the ITextProvider interface, for the + TermControlAutomationPeer. The TermControlAutomationPeer will be attached to + the actual UI tree, via FrameworkElementAutomationPeer. However, the + ControlInteractivity is totally oblivious to the UI tree that might be hosting + it. So this class implements the actual text pattern for the buffer, because + it has access to the buffer. TermControlAutomationPeer can then call the + methods on this class to expose the implementation in the actual UI tree. + +Author(s): +- Mike Griese (migrie), May 2021 + +--*/ + +#pragma once + +#include "ControlInteractivity.h" +#include "InteractivityAutomationPeer.g.h" +#include "../types/TermControlUiaProvider.hpp" +#include "../types/IUiaEventDispatcher.h" +#include "../types/IControlAccessibilityInfo.h" + +namespace winrt::Microsoft::Terminal::Control::implementation +{ + struct InteractivityAutomationPeer : + public InteractivityAutomationPeerT, + ::Microsoft::Console::Types::IUiaEventDispatcher, + ::Microsoft::Console::Types::IControlAccessibilityInfo + { + public: + InteractivityAutomationPeer(Microsoft::Terminal::Control::implementation::ControlInteractivity* owner); + + void SetControlBounds(const Windows::Foundation::Rect bounds); + void SetControlPadding(const Core::Padding padding); + +#pragma region IUiaEventDispatcher + void SignalSelectionChanged() override; + void SignalTextChanged() override; + void SignalCursorChanged() override; +#pragma endregion + +#pragma region ITextProvider Pattern + Windows::UI::Xaml::Automation::Provider::ITextRangeProvider RangeFromPoint(Windows::Foundation::Point screenLocation); + Windows::UI::Xaml::Automation::Provider::ITextRangeProvider RangeFromChild(Windows::UI::Xaml::Automation::Provider::IRawElementProviderSimple childElement); + com_array GetVisibleRanges(); + com_array GetSelection(); + Windows::UI::Xaml::Automation::SupportedTextSelection SupportedTextSelection(); + Windows::UI::Xaml::Automation::Provider::ITextRangeProvider DocumentRange(); +#pragma endregion + +#pragma region IControlAccessibilityInfo Pattern + // Inherited via IControlAccessibilityInfo + virtual COORD GetFontSize() const override; + virtual RECT GetBounds() const override; + virtual RECT GetPadding() const override; + virtual double GetScaleFactor() const override; + virtual void ChangeViewport(SMALL_RECT NewWindow) override; + virtual HRESULT GetHostUiaProvider(IRawElementProviderSimple** provider) override; +#pragma endregion + + TYPED_EVENT(SelectionChanged, IInspectable, IInspectable); + TYPED_EVENT(TextChanged, IInspectable, IInspectable); + TYPED_EVENT(CursorChanged, IInspectable, IInspectable); + + private: + ::Microsoft::WRL::ComPtr<::Microsoft::Terminal::TermControlUiaProvider> _uiaProvider; + winrt::Microsoft::Terminal::Control::implementation::ControlInteractivity* _interactivity; + + til::rectangle _controlBounds{}; + til::rectangle _controlPadding{}; + + winrt::com_array WrapArrayOfTextRangeProviders(SAFEARRAY* textRanges); + }; +} diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.idl b/src/cascadia/TerminalControl/InteractivityAutomationPeer.idl new file mode 100644 index 000000000..4c9039c5c --- /dev/null +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.idl @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Control +{ + [default_interface] runtimeclass InteractivityAutomationPeer : + Windows.UI.Xaml.Automation.Peers.AutomationPeer, + Windows.UI.Xaml.Automation.Provider.ITextProvider + { + + void SetControlBounds(Windows.Foundation.Rect bounds); + void SetControlPadding(Microsoft.Terminal.Core.Padding padding); + + event Windows.Foundation.TypedEventHandler SelectionChanged; + event Windows.Foundation.TypedEventHandler TextChanged; + event Windows.Foundation.TypedEventHandler CursorChanged; + } +} diff --git a/src/cascadia/TerminalControl/KeyChord.cpp b/src/cascadia/TerminalControl/KeyChord.cpp index b8faab323..9144726e7 100644 --- a/src/cascadia/TerminalControl/KeyChord.cpp +++ b/src/cascadia/TerminalControl/KeyChord.cpp @@ -10,46 +10,95 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; namespace winrt::Microsoft::Terminal::Control::implementation { - KeyChord::KeyChord() noexcept : - _modifiers{ 0 }, - _vkey{ 0 } + static VirtualKeyModifiers modifiersFromBooleans(bool ctrl, bool alt, bool shift, bool win) + { + VirtualKeyModifiers modifiers = VirtualKeyModifiers::None; + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Control, ctrl); + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Menu, alt); + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Shift, shift); + WI_SetFlagIf(modifiers, VirtualKeyModifiers::Windows, win); + return modifiers; + } + + KeyChord::KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey, int32_t scanCode) noexcept : + KeyChord(modifiersFromBooleans(ctrl, alt, shift, win), vkey, scanCode) { } - KeyChord::KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey) noexcept : - _modifiers{ (ctrl ? VirtualKeyModifiers::Control : VirtualKeyModifiers::None) | - (alt ? VirtualKeyModifiers::Menu : VirtualKeyModifiers::None) | - (shift ? VirtualKeyModifiers::Shift : VirtualKeyModifiers::None) }, - _vkey{ vkey } - { - } - - KeyChord::KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey) noexcept : - _modifiers{ (ctrl ? VirtualKeyModifiers::Control : VirtualKeyModifiers::None) | - (alt ? VirtualKeyModifiers::Menu : VirtualKeyModifiers::None) | - (shift ? VirtualKeyModifiers::Shift : VirtualKeyModifiers::None) | - (win ? VirtualKeyModifiers::Windows : VirtualKeyModifiers::None) }, - _vkey{ vkey } - { - } - - KeyChord::KeyChord(VirtualKeyModifiers const& modifiers, int32_t vkey) noexcept : + KeyChord::KeyChord(const VirtualKeyModifiers modifiers, int32_t vkey, int32_t scanCode) noexcept : _modifiers{ modifiers }, - _vkey{ vkey } + _vkey{ vkey }, + _scanCode{ scanCode } { + // ActionMap needs to identify KeyChords which should "layer" (overwrite) each other. + // For instance win+sc(41) and win+` both specify the same KeyChord on an US keyboard layout + // from the perspective of a user. Either of the two should correctly overwrite the other. + // We can help ActionMap with this by ensuring that Vkey() is always valid. + if (!_vkey) + { + _vkey = MapVirtualKeyW(scanCode, MAPVK_VSC_TO_VK_EX); + } + + assert(_vkey || _scanCode); } - VirtualKeyModifiers KeyChord::Modifiers() noexcept + uint64_t KeyChord::Hash() const noexcept + { + // Two KeyChords are equal if they have the same modifiers and either identical + // Vkey or ScanCode, with Vkey being preferred. See KeyChord::Equals(). + // This forces us to _either_ hash _vkey or _scanCode. + // + // Additionally the hash value with _vkey==123 and _scanCode==123 must be different. + // --> Taint hashes of KeyChord without _vkey. + auto h = static_cast(_modifiers) << 32; + h |= _vkey ? _vkey : (_scanCode | 0xBABE0000); + + // I didn't like how std::hash uses the byte-wise FNV1a for integers. + // So I built my own std::hash with murmurhash3. + h ^= h >> 33; + h *= UINT64_C(0xff51afd7ed558ccd); + h ^= h >> 33; + h *= UINT64_C(0xc4ceb9fe1a85ec53); + h ^= h >> 33; + + return h; + } + + bool KeyChord::Equals(const Control::KeyChord& other) const noexcept + { + // Two KeyChords are equal if they have the same modifiers and either identical + // Vkey or ScanCode, with Vkey being preferred. Vkey is preferred because: + // ActionMap needs to identify KeyChords which should "layer" (overwrite) each other. + // For instance win+sc(41) and win+` both specify the same KeyChord on an US keyboard layout + // from the perspective of a user. Either of the two should correctly overwrite the other. + // + // Two problems exist here: + // * Since a value of 0 indicates that the Vkey/ScanCode isn't set, we cannot use == to compare them. + // Otherwise we return true, even if the Vkey/ScanCode isn't set on both sides. + // * Whenever Equals() returns true, the Hash() value _must_ be identical. + // For instance the code below ensures the preference of Vkey over ScanCode by: + // this->_vkey || other->_vkey ? ...vkey... : ...scanCode... + // We cannot use "&&", even if it would be technically more correct, as this would mean the + // return value of this function would be dependent on the existence of a Vkey in "other". + // But Hash() has no "other" argument to consider when deciding if its Vkey or ScanCode should be hashed. + // + // Bitwise operators are used because MSVC doesn't support compiling + // boolean operators into bitwise ones at the time of writing. + const auto otherSelf = winrt::get_self(other); + return _modifiers == otherSelf->_modifiers && ((_vkey | otherSelf->_vkey) ? _vkey == otherSelf->_vkey : _scanCode == otherSelf->_scanCode); + } + + VirtualKeyModifiers KeyChord::Modifiers() const noexcept { return _modifiers; } - void KeyChord::Modifiers(VirtualKeyModifiers const& value) noexcept + void KeyChord::Modifiers(const VirtualKeyModifiers value) noexcept { _modifiers = value; } - int32_t KeyChord::Vkey() noexcept + int32_t KeyChord::Vkey() const noexcept { return _vkey; } @@ -58,4 +107,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _vkey = value; } + + int32_t KeyChord::ScanCode() const noexcept + { + return _scanCode; + } + + void KeyChord::ScanCode(int32_t value) noexcept + { + _scanCode = value; + } } diff --git a/src/cascadia/TerminalControl/KeyChord.h b/src/cascadia/TerminalControl/KeyChord.h index d659d03cf..c8348c2fe 100644 --- a/src/cascadia/TerminalControl/KeyChord.h +++ b/src/cascadia/TerminalControl/KeyChord.h @@ -9,19 +9,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation { struct KeyChord : KeyChordT { - KeyChord() noexcept; - KeyChord(winrt::Windows::System::VirtualKeyModifiers const& modifiers, int32_t vkey) noexcept; - KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey) noexcept; - KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey) noexcept; + KeyChord() noexcept = default; + KeyChord(const winrt::Windows::System::VirtualKeyModifiers modifiers, int32_t vkey, int32_t scanCode) noexcept; + KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey, int32_t scanCode) noexcept; - winrt::Windows::System::VirtualKeyModifiers Modifiers() noexcept; - void Modifiers(winrt::Windows::System::VirtualKeyModifiers const& value) noexcept; - int32_t Vkey() noexcept; + uint64_t Hash() const noexcept; + bool Equals(const Control::KeyChord& other) const noexcept; + + winrt::Windows::System::VirtualKeyModifiers Modifiers() const noexcept; + void Modifiers(const winrt::Windows::System::VirtualKeyModifiers value) noexcept; + int32_t Vkey() const noexcept; void Vkey(int32_t value) noexcept; + int32_t ScanCode() const noexcept; + void ScanCode(int32_t value) noexcept; private: - winrt::Windows::System::VirtualKeyModifiers _modifiers; - int32_t _vkey; + winrt::Windows::System::VirtualKeyModifiers _modifiers{}; + int32_t _vkey{}; + int32_t _scanCode{}; }; } diff --git a/src/cascadia/TerminalControl/KeyChord.idl b/src/cascadia/TerminalControl/KeyChord.idl index a1df50ddd..3b8e662ed 100644 --- a/src/cascadia/TerminalControl/KeyChord.idl +++ b/src/cascadia/TerminalControl/KeyChord.idl @@ -7,11 +7,14 @@ namespace Microsoft.Terminal.Control runtimeclass KeyChord { KeyChord(); - KeyChord(Windows.System.VirtualKeyModifiers modifiers, Int32 vkey); - KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Int32 vkey); - KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Boolean win, Int32 vkey); + KeyChord(Windows.System.VirtualKeyModifiers modifiers, Int32 vkey, Int32 scanCode); + KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Boolean win, Int32 vkey, Int32 scanCode); + + UInt64 Hash(); + Boolean Equals(KeyChord other); Windows.System.VirtualKeyModifiers Modifiers; Int32 Vkey; + Int32 ScanCode; } } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 1894c7b65..e36fadd6a 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include "../../types/inc/GlyphWidth.hpp" #include "../../types/inc/Utils.hpp" @@ -34,7 +33,8 @@ using namespace winrt::Windows::ApplicationModel::DataTransfer; constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8); // The minimum delay between updating the TSF input control. -constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); +// This is already throttled primarily in the ControlCore, with a timeout of 100ms. We're adding another smaller one here, as the (potentially x-proc) call will come in off the UI thread +constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(8); // The minimum delay between updating the locations of regex patterns constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); @@ -44,6 +44,8 @@ constexpr const auto TerminalWarningBellInterval = std::chrono::milliseconds(100 DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::CopyFormat); +DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::MouseButtonState); + namespace winrt::Microsoft::Terminal::Control::implementation { TermControl::TermControl(IControlSettings settings, @@ -60,33 +62,29 @@ namespace winrt::Microsoft::Terminal::Control::implementation { InitializeComponent(); - _interactivity = winrt::make_self(settings, connection); - _core = _interactivity->GetCore(); - - // Use a manual revoker on the output event, so we can immediately stop - // worrying about it on destruction. - _coreOutputEventToken = _core->ReceivedOutput({ this, &TermControl::_coreReceivedOutput }); + _interactivity = winrt::make(settings, connection); + _core = _interactivity.Core(); // These events might all be triggered by the connection, but that // should be drained and closed before we complete destruction. So these // are safe. - _core->ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); - _core->WarningBell({ this, &TermControl::_coreWarningBell }); - _core->CursorPositionChanged({ this, &TermControl::_CursorPositionChanged }); + _core.ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); + _core.WarningBell({ this, &TermControl::_coreWarningBell }); + _core.CursorPositionChanged({ this, &TermControl::_CursorPositionChanged }); // This event is specifically triggered by the renderer thread, a BG thread. Use a weak ref here. - _core->RendererEnteredErrorState({ get_weak(), &TermControl::_RendererEnteredErrorState }); + _core.RendererEnteredErrorState({ get_weak(), &TermControl::_RendererEnteredErrorState }); // These callbacks can only really be triggered by UI interactions. So // they don't need weak refs - they can't be triggered unless we're // alive. - _core->BackgroundColorChanged({ this, &TermControl::_BackgroundColorChangedHandler }); - _core->FontSizeChanged({ this, &TermControl::_coreFontSizeChanged }); - _core->TransparencyChanged({ this, &TermControl::_coreTransparencyChanged }); - _core->RaiseNotice({ this, &TermControl::_coreRaisedNotice }); - _core->HoveredHyperlinkChanged({ this, &TermControl::_hoveredHyperlinkChanged }); - _interactivity->OpenHyperlink({ this, &TermControl::_HyperlinkHandler }); - _interactivity->ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); + _core.BackgroundColorChanged({ this, &TermControl::_BackgroundColorChangedHandler }); + _core.FontSizeChanged({ this, &TermControl::_coreFontSizeChanged }); + _core.TransparencyChanged({ this, &TermControl::_coreTransparencyChanged }); + _core.RaiseNotice({ this, &TermControl::_coreRaisedNotice }); + _core.HoveredHyperlinkChanged({ this, &TermControl::_hoveredHyperlinkChanged }); + _interactivity.OpenHyperlink({ this, &TermControl::_HyperlinkHandler }); + _interactivity.ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); // Initialize the terminal only once the swapchainpanel is loaded - that // way, we'll be able to query the real pixel size it got on layout @@ -103,37 +101,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation } }); - // Many of these ThrottledFunc's should be inside ControlCore. However, - // currently they depend on the Dispatcher() of the UI thread, which the - // Core eventually won't have access to. When we get to - // https://github.com/microsoft/terminal/projects/5#card-50760282 - // then we'll move the applicable ones. - // - // These four throttled functions are triggered by terminal output and interact with the UI. + // Get our dispatcher. This will get us the same dispatcher as + // TermControl::Dispatcher(). + auto dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + + // These three throttled functions are triggered by terminal output and interact with the UI. // Since Close() is the point after which we are removed from the UI, but before the // destructor has run, we MUST check control->_IsClosing() before actually doing anything. - _tsfTryRedrawCanvas = std::make_shared>( - Dispatcher(), - TsfRedrawInterval, - [weakThis = get_weak()]() { - if (auto control{ weakThis.get() }; !control->_IsClosing()) - { - control->TSFInputControl().TryRedrawCanvas(); - } - }); - - _updatePatternLocations = std::make_shared>( - Dispatcher(), - UpdatePatternLocationsInterval, - [weakThis = get_weak()]() { - if (auto control{ weakThis.get() }; !control->_IsClosing()) - { - control->_core->UpdatePatternLocations(); - } - }); - _playWarningBell = std::make_shared( - Dispatcher(), + dispatcher, TerminalWarningBellInterval, [weakThis = get_weak()]() { if (auto control{ weakThis.get() }; !control->_IsClosing()) @@ -143,7 +119,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation }); _updateScrollBar = std::make_shared>( - Dispatcher(), + dispatcher, ScrollBarUpdateInterval, [weakThis = get_weak()](const auto& update) { if (auto control{ weakThis.get() }; !control->_IsClosing()) @@ -187,16 +163,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation // If a text is selected inside terminal, use it to populate the search box. // If the search box already contains a value, it will be overridden. - if (_core->HasSelection()) + if (_core.HasSelection()) { // Currently we populate the search box only if a single line is selected. // Empirically, multi-line selection works as well on sample scenarios, // but since code paths differ, extra work is required to ensure correctness. - auto bufferText = _core->SelectedText(true); - if (bufferText.size() == 1) + auto bufferText = _core.SelectedText(true); + if (bufferText.Size() == 1) { - const auto selectedLine{ til::at(bufferText, 0) }; - _searchBox->PopulateTextbox(selectedLine.data()); + const auto selectedLine{ bufferText.GetAt(0) }; + _searchBox->PopulateTextbox(selectedLine); } } @@ -217,7 +193,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else { - _core->Search(_searchBox->TextBox().Text(), goForward, false); + _core.Search(_searchBox->TextBox().Text(), goForward, false); } } @@ -234,7 +210,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const bool goForward, const bool caseSensitive) { - _core->Search(text, goForward, caseSensitive); + _core.Search(text, goForward, caseSensitive); } // Method Description: @@ -303,7 +279,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - _core->UpdateSettings(_settings); + _core.UpdateSettings(_settings); // Update our control settings _ApplyUISettings(_settings); @@ -362,7 +338,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation foregroundBrush.Color(static_cast(newAppearance.DefaultForeground())); TSFInputControl().Foreground(foregroundBrush); - _core->UpdateAppearance(newAppearance); + _core.UpdateAppearance(newAppearance); } // Method Description: @@ -373,12 +349,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void TermControl::SendInput(const winrt::hstring& wstr) { - _core->SendInput(wstr); + _core.SendInput(wstr); } void TermControl::ToggleShaderEffects() { - _core->ToggleShaderEffects(); + _core.ToggleShaderEffects(); } // Method Description: @@ -401,7 +377,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _changeBackgroundColor(bg); // Apply padding as swapChainPanel's margin - auto newMargin = _ParseThicknessFromPadding(newSettings.Padding()); + const auto newMargin = ParseThicknessFromPadding(newSettings.Padding()); SwapChainPanel().Margin(newMargin); TSFInputControl().Margin(newMargin); @@ -422,7 +398,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation ScrollBar().Visibility(Visibility::Visible); } - _interactivity->UpdateSettings(); + _interactivity.UpdateSettings(); + if (_automationPeer) + { + _automationPeer.SetControlPadding(Core::Padding{ newMargin.Left, + newMargin.Top, + newMargin.Right, + newMargin.Bottom }); + } } // Method Description: @@ -471,7 +454,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // GH#5098: Inform the engine of the new opacity of the default text background. - _core->SetBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); + _core.SetBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); } else { @@ -479,7 +462,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation RootGrid().Background(solidColor); // GH#5098: Inform the engine of the new opacity of the default text background. - _core->SetBackgroundOpacity(1.0f); + _core.SetBackgroundOpacity(1.0f); } } @@ -492,7 +475,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_BackgroundColorChangedHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) { - til::color newBgColor{ _core->BackgroundColor() }; + til::color newBgColor{ _core.BackgroundColor() }; _changeBackgroundColor(newBgColor); } @@ -527,37 +510,33 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Return Value: // - The automation peer for our control Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer() - try { - if (_initializedTerminal && !_IsClosing()) // only set up the automation peer if we're ready to go live + // MSFT 33353327: We're purposefully not using _initializedTerminal to ensure we're fully initialized. + // Doing so makes us return nullptr when XAML requests an automation peer. + // Instead, we need to give XAML an automation peer, then fix it later. + if (!_IsClosing()) { // create a custom automation peer with this code pattern: // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) - auto autoPeer = winrt::make_self(this); - - _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get()); - _core->AttachUiaEngine(_uiaEngine.get()); - _automationPeer = *autoPeer; - return _automationPeer; + if (const auto& interactivityAutoPeer{ _interactivity.OnCreateAutomationPeer() }) + { + const auto margins{ SwapChainPanel().Margin() }; + const Core::Padding padding{ margins.Left, + margins.Top, + margins.Right, + margins.Bottom }; + _automationPeer = winrt::make(this, padding, interactivityAutoPeer); + return _automationPeer; + } } return nullptr; } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - return nullptr; - } - - ::Microsoft::Console::Types::IUiaData* TermControl::GetUiaData() const - { - return _core->GetUiaData(); - } // This is needed for TermControlAutomationPeer. We probably could find a // clever way around asking the core for this. til::point TermControl::GetFontSize() const { - return _core->GetFont().GetSize(); + return til::point{ til::math::rounding, _core.FontSize().Width, _core.FontSize().Height }; } const Windows::UI::Xaml::Thickness TermControl::GetPadding() @@ -567,7 +546,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TerminalConnection::ConnectionState TermControl::ConnectionState() const { - return _core->ConnectionState(); + return _core.ConnectionState(); } winrt::fire_and_forget TermControl::RenderEngineSwapChainChanged(IInspectable /*sender*/, IInspectable /*args*/) @@ -581,7 +560,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (auto control{ weakThis.get() }) { - const auto chainHandle = _core->GetSwapChainHandle(); + const HANDLE chainHandle = reinterpret_cast(control->_core.SwapChainHandle()); _AttachDxgiSwapChainToXaml(chainHandle); } } @@ -658,23 +637,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation // after Enable, then it'll be possible to paint the frame once // _before_ the warning handler is set up, and then warnings from // the first paint will be ignored! - _core->RendererWarning({ get_weak(), &TermControl::_RendererWarning }); + _core.RendererWarning({ get_weak(), &TermControl::_RendererWarning }); - const auto coreInitialized = _core->Initialize(panelWidth, - panelHeight, - panelScaleX); + const auto coreInitialized = _core.Initialize(panelWidth, + panelHeight, + panelScaleX); if (!coreInitialized) { return false; } - _interactivity->Initialize(); + _interactivity.Initialize(); - _AttachDxgiSwapChainToXaml(_core->GetSwapChainHandle()); + _AttachDxgiSwapChainToXaml(reinterpret_cast(_core.SwapChainHandle())); // Tell the DX Engine to notify us when the swap chain changes. We do // this after we initially set the swapchain so as to avoid unnecessary // callbacks (and locking problems) - _core->SwapChainChanged({ get_weak(), &TermControl::RenderEngineSwapChainChanged }); + _core.SwapChainChanged({ get_weak(), &TermControl::RenderEngineSwapChainChanged }); // !! LOAD BEARING !! // Make sure you enable painting _AFTER_ calling _AttachDxgiSwapChainToXaml @@ -684,9 +663,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // issues where the Renderer starts trying to paint before we've // actually attached the swapchain to anything, and the DxEngine is not // prepared to handle that. - _core->EnablePainting(); + _core.EnablePainting(); - auto bufferHeight = _core->BufferHeight(); + auto bufferHeight = _core.BufferHeight(); ScrollBar().Maximum(bufferHeight - bufferHeight); ScrollBar().Minimum(0); @@ -702,8 +681,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation DispatcherTimer cursorTimer; cursorTimer.Interval(std::chrono::milliseconds(blinkTime)); cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick }); - cursorTimer.Start(); _cursorTimer.emplace(std::move(cursorTimer)); + // As of GH#6586, don't start the cursor timer immediately, and + // don't show the cursor initially. We'll show the cursor and start + // the timer when the control is first focused. cursorTimer.Start(); + _core.CursorOn(false); } else { @@ -732,14 +714,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Now that the renderer is set up, update the appearance for initialization _UpdateAppearanceFromUIThread(_settings); - // Focus the control here. If we do it during control initialization, then - // focus won't actually get passed to us. I believe this is because - // we're not technically a part of the UI tree yet, so focusing us - // becomes a no-op. - this->Focus(FocusState::Programmatic); - _initializedTerminal = true; + // MSFT 33353327: If the AutomationPeer was created before we were done initializing, + // make sure it's properly set up now. + if (_automationPeer) + { + _automationPeer.UpdateControlBounds(); + const auto margins{ GetPadding() }; + _automationPeer.SetControlPadding(Core::Padding{ margins.Left, + margins.Top, + margins.Right, + margins.Bottom }); + } + // Likewise, run the event handlers outside of lock (they could // be reentrant) _InitializedHandlers(*this, nullptr); @@ -763,7 +751,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { modifiers |= ControlKeyStates::EnhancedKey; } - const bool handled = _core->SendCharEvent(ch, scanCode, modifiers); + const bool handled = _core.SendCharEvent(ch, scanCode, modifiers); e.Handled(handled); } @@ -775,7 +763,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) { // Short-circuit isReadOnly check to avoid warning dialog - if (_core->IsInReadOnlyMode()) + if (_core.IsInReadOnlyMode()) { return false; } @@ -790,26 +778,39 @@ namespace winrt::Microsoft::Terminal::Control::implementation (void)_TrySendKeyEvent(VK_MENU, scanCode, modifiers, false); handled = true; } - else if (vkey == VK_F7 && down) + else if ((vkey == VK_F7 || vkey == VK_SPACE) && down) { // Manually generate an F7 event into the key bindings or terminal. // This is required as part of GH#638. + // Or do so for alt+space; only send to terminal when explicitly unbound + // That is part of #GH7125 auto bindings{ _settings.KeyBindings() }; + bool isUnbound = false; + const KeyChord kc = { + modifiers.IsCtrlPressed(), + modifiers.IsAltPressed(), + modifiers.IsShiftPressed(), + modifiers.IsWinPressed(), + gsl::narrow_cast(vkey), + 0 + }; if (bindings) { - handled = bindings.TryKeyChord({ - modifiers.IsCtrlPressed(), - modifiers.IsAltPressed(), - modifiers.IsShiftPressed(), - VK_F7, - }); + handled = bindings.TryKeyChord(kc); + + if (!handled) + { + isUnbound = bindings.IsKeyChordExplicitlyUnbound(kc); + } } - if (!handled) + const bool sendToTerminal = vkey == VK_F7 || (vkey == VK_SPACE && isUnbound); + + if (!handled && sendToTerminal) { // _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason. - (void)_TrySendKeyEvent(VK_F7, scanCode, modifiers, true); + (void)_TrySendKeyEvent(gsl::narrow_cast(vkey), scanCode, modifiers, true); // GH#6438: Note that we're _not_ sending the key up here - that'll // get passed through XAML to our KeyUp handler normally. handled = true; @@ -861,7 +862,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); // Short-circuit isReadOnly check to avoid warning dialog - if (_core->IsInReadOnlyMode()) + if (_core.IsInReadOnlyMode()) { e.Handled(!keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers)); return; @@ -931,6 +932,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation modifiers.IsShiftPressed(), modifiers.IsWinPressed(), vkey, + scanCode, }); if (!success) { @@ -993,17 +995,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation // This will prevent the system from trying to get the character out // of it and sending us a CharacterReceived event. const auto handled = vkey ? - _core->TrySendKeyEvent(vkey, - scanCode, - modifiers, - keyDown) : + _core.TrySendKeyEvent(vkey, + scanCode, + modifiers, + keyDown) : true; if (_cursorTimer) { // Manually show the cursor when a key is pressed. Restarting // the timer prevents flickering. - _core->CursorOn(true); + _core.CursorOn(true); _cursorTimer->Start(); } @@ -1053,20 +1055,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation Focus(FocusState::Pointer); } + // Mark that this pointer event actually started within our bounds. + // We'll need this later, for PointerMoved events. + _pointerPressedInBounds = true; + if (type == Windows::Devices::Input::PointerDeviceType::Touch) { const auto contactRect = point.Properties().ContactRect(); auto anchor = til::point{ til::math::rounding, contactRect.X, contactRect.Y }; - _interactivity->TouchPressed(anchor); + _interactivity.TouchPressed(anchor); } else { const auto cursorPosition = point.Position(); - _interactivity->PointerPressed(TermControl::GetPressedMouseButtons(point), - TermControl::GetPointerUpdateKind(point), - point.Timestamp(), - ControlKeyStates{ args.KeyModifiers() }, - _toTerminalOrigin(cursorPosition)); + _interactivity.PointerPressed(TermControl::GetPressedMouseButtons(point), + TermControl::GetPointerUpdateKind(point), + point.Timestamp(), + ControlKeyStates{ args.KeyModifiers() }, + _toTerminalOrigin(cursorPosition)); } args.Handled(true); @@ -1101,14 +1107,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (type == Windows::Devices::Input::PointerDeviceType::Mouse || type == Windows::Devices::Input::PointerDeviceType::Pen) { - _interactivity->PointerMoved(TermControl::GetPressedMouseButtons(point), - TermControl::GetPointerUpdateKind(point), - ControlKeyStates(args.KeyModifiers()), - _focused, - pixelPosition); + _interactivity.PointerMoved(TermControl::GetPressedMouseButtons(point), + TermControl::GetPointerUpdateKind(point), + ControlKeyStates(args.KeyModifiers()), + _focused, + pixelPosition, + _pointerPressedInBounds); - if (_focused && point.Properties().IsLeftButtonPressed()) + // GH#9109 - Only start an auto-scroll when the drag actually + // started within our bounds. Otherwise, someone could start a drag + // outside the terminal control, drag into the padding, and trick us + // into starting to scroll. + if (_focused && _pointerPressedInBounds && point.Properties().IsLeftButtonPressed()) { + // We want to find the distance relative to the bounds of the + // SwapChainPanel, not the entire control. If they drag out of + // the bounds of the text, into the padding, we still what that + // to auto-scroll const double cursorBelowBottomDist = cursorPosition.Y - SwapChainPanel().Margin().Top - SwapChainPanel().ActualHeight(); const double cursorAboveTopDist = -1 * cursorPosition.Y + SwapChainPanel().Margin().Top; @@ -1138,7 +1153,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto contactRect = point.Properties().ContactRect(); til::point newTouchPoint{ til::math::rounding, contactRect.X, contactRect.Y }; - _interactivity->TouchMoved(newTouchPoint, _focused); + _interactivity.TouchMoved(newTouchPoint, _focused); } args.Handled(true); @@ -1158,6 +1173,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } + _pointerPressedInBounds = false; + const auto ptr = args.Pointer(); const auto point = args.GetCurrentPoint(*this); const auto cursorPosition = point.Position(); @@ -1169,14 +1186,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (type == Windows::Devices::Input::PointerDeviceType::Mouse || type == Windows::Devices::Input::PointerDeviceType::Pen) { - _interactivity->PointerReleased(TermControl::GetPressedMouseButtons(point), - TermControl::GetPointerUpdateKind(point), - ControlKeyStates(args.KeyModifiers()), - pixelPosition); + _interactivity.PointerReleased(TermControl::GetPressedMouseButtons(point), + TermControl::GetPointerUpdateKind(point), + ControlKeyStates(args.KeyModifiers()), + pixelPosition); } else if (type == Windows::Devices::Input::PointerDeviceType::Touch) { - _interactivity->TouchReleased(); + _interactivity.TouchReleased(); } _TryStopAutoScroll(ptr.PointerId()); @@ -1203,11 +1220,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation _RestorePointerCursorHandlers(*this, nullptr); const auto point = args.GetCurrentPoint(*this); + // GH#10329 - we don't need to handle horizontal scrolls. Only vertical ones. + // So filter out the horizontal ones. + if (point.Properties().IsHorizontalMouseWheel()) + { + return; + } - auto result = _interactivity->MouseWheel(ControlKeyStates{ args.KeyModifiers() }, - point.Properties().MouseWheelDelta(), - _toTerminalOrigin(point.Position()), - TermControl::GetPressedMouseButtons(point)); + auto result = _interactivity.MouseWheel(ControlKeyStates{ args.KeyModifiers() }, + point.Properties().MouseWheelDelta(), + _toTerminalOrigin(point.Position()), + TermControl::GetPressedMouseButtons(point)); if (result) { args.Handled(true); @@ -1231,10 +1254,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation const bool rightButtonDown) { const auto modifiers = _GetPressedModifierKeys(); - TerminalInput::MouseButtonState state{ leftButtonDown, - midButtonDown, - rightButtonDown }; - return _interactivity->MouseWheel(modifiers, delta, _toTerminalOrigin(location), state); + + Control::MouseButtonState state{}; + WI_SetFlagIf(state, Control::MouseButtonState::IsLeftButtonDown, leftButtonDown); + WI_SetFlagIf(state, Control::MouseButtonState::IsMiddleButtonDown, midButtonDown); + WI_SetFlagIf(state, Control::MouseButtonState::IsRightButtonDown, rightButtonDown); + + return _interactivity.MouseWheel(modifiers, delta, _toTerminalOrigin(location), state); } // Method Description: @@ -1259,30 +1285,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation CATCH_LOG(); } - void TermControl::_coreReceivedOutput(const IInspectable& /*sender*/, - const IInspectable& /*args*/) - { - // Queue up a throttled UpdatePatternLocations call. In the future, we - // should have the _updatePatternLocations ThrottledFunc internal to - // ControlCore, and run on that object's dispatcher queue. - // - // We're not doing that quite yet, because the Core will eventually - // be out-of-proc from the UI thread, and won't be able to just use - // the UI thread as the dispatcher queue thread. - // - // THIS IS CALLED ON EVERY STRING OF TEXT OUTPUT TO THE TERMINAL. Think - // twice before adding anything here. - - _updatePatternLocations->Run(); - } - // Method Description: // - Reset the font size of the terminal to its default size. // Arguments: // - none void TermControl::ResetFontSize() { - _core->ResetFontSize(); + _core.ResetFontSize(); } // Method Description: @@ -1291,7 +1300,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - fontSizeDelta: The amount to increase or decrease the font size by. void TermControl::AdjustFontSize(int fontSizeDelta) { - _core->AdjustFontSize(fontSizeDelta); + _core.AdjustFontSize(fontSizeDelta); } void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, @@ -1306,15 +1315,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto newValue = args.NewValue(); - _interactivity->UpdateScrollbar(newValue); + _interactivity.UpdateScrollbar(newValue); // User input takes priority over terminal events so cancel // any pending scroll bar update if the user scrolls. _updateScrollBar->ModifyPending([](auto& update) { update.newValue.reset(); }); - - _updatePatternLocations->Run(); } // Method Description: @@ -1452,10 +1459,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // GH#5421: Enable the UiaEngine before checking for the SearchBox // That way, new selections are notified to automation clients. - if (_uiaEngine.get()) - { - THROW_IF_FAILED(_uiaEngine->Enable()); - } + // The _uiaEngine lives in _interactivity, so call into there to enable it. + _interactivity.GotFocus(); // If the searchbox is focused, we don't want TSFInputControl to think // it has focus so it doesn't intercept IME input. We also don't want the @@ -1473,7 +1478,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_cursorTimer) { // When the terminal focuses, show the cursor immediately - _core->CursorOn(true); + _core.CursorOn(true); _cursorTimer->Start(); } @@ -1482,8 +1487,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _blinkTimer->Start(); } - _interactivity->GainFocus(); - // Only update the appearance here if an unfocused config exists - // if an unfocused config does not exist then we never would have switched // appearances anyway so there's no need to switch back upon gaining focus @@ -1509,10 +1512,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation _focused = false; - if (_uiaEngine.get()) - { - THROW_IF_FAILED(_uiaEngine->Disable()); - } + // This will disable the accessibility notifications, because the + // UiaEngine lives in ControlInteractivity + _interactivity.LostFocus(); if (TSFInputControl() != nullptr) { @@ -1522,7 +1524,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_cursorTimer) { _cursorTimer->Stop(); - _core->CursorOn(false); + _core.CursorOn(false); } if (_blinkTimer) @@ -1552,7 +1554,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto newSize = e.NewSize(); - _core->SizeChanged(newSize.Width, newSize.Height); + _core.SizeChanged(newSize.Width, newSize.Height); + + if (_automationPeer) + { + _automationPeer.UpdateControlBounds(); + } } // Method Description: @@ -1587,7 +1594,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { const auto scaleX = sender.CompositionScaleX(); - _core->ScaleChanged(scaleX); + _core.ScaleChanged(scaleX); } // Method Description: @@ -1600,7 +1607,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (!_IsClosing()) { - _core->BlinkCursor(); + _core.BlinkCursor(); } } @@ -1614,7 +1621,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (!_IsClosing()) { - _core->BlinkAttributeTick(); + _core.BlinkAttributeTick(); } } @@ -1624,7 +1631,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - cursorPosition: in pixels, relative to the origin of the control void TermControl::_SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition) { - _interactivity->SetEndSelectionPoint(_toTerminalOrigin(cursorPosition)); + _interactivity.SetEndSelectionPoint(_toTerminalOrigin(cursorPosition)); } // Method Description: @@ -1648,7 +1655,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation update.newValue = args.ViewTop(); _updateScrollBar->Run(update); - _updatePatternLocations->Run(); } // Method Description: @@ -1656,15 +1662,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation // to be where the current cursor position is. // Arguments: // - N/A - void TermControl::_CursorPositionChanged(const IInspectable& /*sender*/, - const IInspectable& /*args*/) + winrt::fire_and_forget TermControl::_CursorPositionChanged(const IInspectable& /*sender*/, + const IInspectable& /*args*/) { - _tsfTryRedrawCanvas->Run(); + // Prior to GH#10187, this fired a trailing throttled func to update the + // TSF canvas only every 100ms. Now, the throttling occurs on the + // ControlCore side. If we're told to update the cursor position, we can + // just go ahead and do it. + // This can come in off the COM thread - hop back to the UI thread. + auto weakThis{ get_weak() }; + co_await resume_foreground(Dispatcher()); + if (auto control{ weakThis.get() }; !control->_IsClosing()) + { + control->TSFInputControl().TryRedrawCanvas(); + } } hstring TermControl::Title() { - return _core->Title(); + return _core.Title(); } hstring TermControl::GetProfileName() const @@ -1674,12 +1690,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring TermControl::WorkingDirectory() const { - return _core->WorkingDirectory(); + return _core.WorkingDirectory(); } bool TermControl::BracketedPasteEnabled() const noexcept { - return _core->BracketedPasteEnabled(); + return _core.BracketedPasteEnabled(); } // Method Description: @@ -1697,14 +1713,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation return false; } - return _interactivity->CopySelectionToClipboard(singleLine, formats); + return _interactivity.CopySelectionToClipboard(singleLine, formats); } // Method Description: // - Initiate a paste operation. void TermControl::PasteTextFromClipboard() { - _interactivity->RequestPasteTextFromClipboard(); + _interactivity.RequestPasteTextFromClipboard(); } void TermControl::Close() @@ -1713,13 +1729,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _closing = true; - _core->ReceivedOutput(_coreOutputEventToken); _RestorePointerCursorHandlers(*this, nullptr); + // Disconnect the TSF input control so it doesn't receive EditContext events. TSFInputControl().Close(); _autoScrollTimer.Stop(); - _core->Close(); + _core.Close(); } } @@ -1732,9 +1748,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation ScrollBar().Value(viewTop); } - int TermControl::ScrollOffset() + int TermControl::ScrollOffset() const { - return _core->ScrollOffset(); + return _core.ScrollOffset(); } // Function Description: @@ -1743,7 +1759,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The height of the terminal in lines of text int TermControl::ViewHeight() const { - return _core->ViewHeight(); + return _core.ViewHeight(); + } + + int TermControl::BufferHeight() const + { + return _core.BufferHeight(); } // Function Description: @@ -1844,7 +1865,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } double height = rows * fontSize.Y; - auto thickness = _ParseThicknessFromPadding(padding); + const auto thickness = ParseThicknessFromPadding(padding); // GH#2061 - make sure to account for the size the padding _will be_ scaled to width += scale * (thickness.Left + thickness.Right); height += scale * (thickness.Top + thickness.Bottom); @@ -1862,8 +1883,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The dimensions of a single character of this control, in DIPs winrt::Windows::Foundation::Size TermControl::CharacterDimensions() const { - const auto fontSize = _core->GetFont().GetSize(); - return { gsl::narrow_cast(fontSize.X), gsl::narrow_cast(fontSize.Y) }; + return _core.FontSize(); } // Method Description: @@ -1879,9 +1899,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (_initializedTerminal) { - const auto fontSize = _core->GetFont().GetSize(); - double width = fontSize.X; - double height = fontSize.Y; + const auto fontSize = _core.FontSize(); + double width = fontSize.Width; + double height = fontSize.Height; // Reserve additional space if scrollbar is intended to be visible if (_settings.ScrollState() == ScrollbarState::Visible) { @@ -1924,8 +1944,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - A dimension that would be aligned to the character grid. float TermControl::SnapDimensionToGrid(const bool widthOrHeight, const float dimension) { - const auto fontSize = _core->GetFont().GetSize(); - const auto fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; + const auto fontSize = _core.FontSize(); + const auto fontDimension = widthOrHeight ? fontSize.Width : fontSize.Height; const auto padding = GetPadding(); auto nonTerminalArea = gsl::narrow_cast(widthOrHeight ? @@ -1952,7 +1972,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Four Double values provide independent padding for 4 sides of the bounding rectangle // Return Value: // - Windows::UI::Xaml::Thickness object - Windows::UI::Xaml::Thickness TermControl::_ParseThicknessFromPadding(const hstring padding) + Windows::UI::Xaml::Thickness TermControl::ParseThicknessFromPadding(const hstring padding) { const wchar_t singleCharDelim = L','; std::wstringstream tokenStream(padding.c_str()); @@ -2085,7 +2105,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - _core->SendInput(text); + _core.SendInput(text); } // Method Description: @@ -2105,7 +2125,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - const til::point cursorPos = _core->CursorPosition(); + const til::point cursorPos = _core.CursorPosition(); Windows::Foundation::Point p = { ::base::ClampedNumeric(cursorPos.x()), ::base::ClampedNumeric(cursorPos.y()) }; eventArgs.CurrentPosition(p); @@ -2121,11 +2141,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs) { - const auto fontInfo = _core->GetFont(); eventArgs.FontSize(CharacterDimensions()); - eventArgs.FontFace(fontInfo.GetFaceName()); + eventArgs.FontFace(_core.FontFaceName()); ::winrt::Windows::UI::Text::FontWeight weight; - weight.Weight = static_cast(fontInfo.GetWeight()); + weight.Weight = _core.FontWeight(); eventArgs.FontWeight(weight); } @@ -2167,7 +2186,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation try { Windows::Foundation::Uri link{ co_await e.DataView().GetApplicationLinkAsync() }; - _core->PasteText(link.AbsoluteUri()); + _core.PasteText(link.AbsoluteUri()); } CATCH_LOG(); } @@ -2176,7 +2195,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation try { Windows::Foundation::Uri link{ co_await e.DataView().GetWebLinkAsync() }; - _core->PasteText(link.AbsoluteUri()); + _core.PasteText(link.AbsoluteUri()); } CATCH_LOG(); } @@ -2185,7 +2204,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation try { auto text{ co_await e.DataView().GetTextAsync() }; - _core->PasteText(text); + _core.PasteText(text); } CATCH_LOG(); } @@ -2226,7 +2245,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation allPaths += fullPath; } - _core->PasteText(winrt::hstring{ allPaths }); + _core.PasteText(winrt::hstring{ allPaths }); } } } @@ -2321,7 +2340,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { // It's already loaded if we get here, so just hide it. RendererFailedNotice().Visibility(Visibility::Collapsed); - _core->ResumeRendering(); + _core.ResumeRendering(); } IControlSettings TermControl::Settings() const @@ -2340,25 +2359,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation // hypothetical future where we allow an application to set the tab // color with VT sequences like they're currently allowed to with the // title. - return _core->TabColor(); + return _core.TabColor(); } // Method Description: // - Gets the internal taskbar state value // Return Value: // - The taskbar state of this control - const size_t TermControl::TaskbarState() const noexcept + const uint64_t TermControl::TaskbarState() const noexcept { - return _core->TaskbarState(); + return _core.TaskbarState(); } // Method Description: // - Gets the internal taskbar progress value // Return Value: // - The taskbar progress of this control - const size_t TermControl::TaskbarProgress() const noexcept + const uint64_t TermControl::TaskbarProgress() const noexcept { - return _core->TaskbarProgress(); + return _core.TaskbarProgress(); } void TermControl::BellLightOn() @@ -2418,15 +2437,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - True if the mode is read-only bool TermControl::ReadOnly() const noexcept { - return _core->IsInReadOnlyMode(); + return _core.IsInReadOnlyMode(); } // Method Description: // - Toggles the read-only flag, raises event describing the value change void TermControl::ToggleReadOnly() { - _core->ToggleReadOnlyMode(); - _ReadOnlyChangedHandlers(*this, winrt::box_value(_core->IsInReadOnlyMode())); + _core.ToggleReadOnlyMode(); + _ReadOnlyChangedHandlers(*this, winrt::box_value(_core.IsInReadOnlyMode())); } // Method Description: @@ -2435,9 +2454,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Arguments: // - sender: not used // - args: event data - void TermControl::_PointerExitedHandler(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& /*e*/) + void TermControl::_PointerExitedHandler(Windows::Foundation::IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::PointerRoutedEventArgs const& /*e*/) { - _core->UpdateHoveredCell(std::nullopt); + _core.ClearHoveredCell(); } winrt::fire_and_forget TermControl::_hoveredHyperlinkChanged(IInspectable sender, @@ -2447,10 +2467,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation co_await resume_foreground(Dispatcher()); if (auto self{ weakThis.get() }) { - auto lastHoveredCell = _core->GetHoveredCell(); + auto lastHoveredCell = _core.HoveredCell(); if (lastHoveredCell) { - const auto uriText = _core->GetHoveredUriText(); + const auto uriText = _core.HoveredUriText(); if (!uriText.empty()) { // Update the tooltip with the URI @@ -2465,8 +2485,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Compute the location of the top left corner of the cell in DIPS const til::size marginsInDips{ til::math::rounding, GetPadding().Left, GetPadding().Top }; - const til::point startPos{ *lastHoveredCell }; - const til::size fontSize{ _core->GetFont().GetSize() }; + const til::point startPos{ lastHoveredCell.Value() }; + const til::size fontSize{ til::math::rounding, _core.FontSize() }; const til::point posInPixels{ startPos * fontSize }; const til::point posInDIPs{ posInPixels / SwapChainPanel().CompositionScaleX() }; const til::point locationInDIPs{ posInDIPs + marginsInDips }; @@ -2502,11 +2522,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation _RaiseNoticeHandlers(*this, eventArgs); } - TerminalInput::MouseButtonState TermControl::GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point) + Control::MouseButtonState TermControl::GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point) { - return TerminalInput::MouseButtonState{ point.Properties().IsLeftButtonPressed(), - point.Properties().IsMiddleButtonPressed(), - point.Properties().IsRightButtonPressed() }; + Control::MouseButtonState state{}; + WI_SetFlagIf(state, Control::MouseButtonState::IsLeftButtonDown, point.Properties().IsLeftButtonPressed()); + WI_SetFlagIf(state, Control::MouseButtonState::IsMiddleButtonDown, point.Properties().IsMiddleButtonPressed()); + WI_SetFlagIf(state, Control::MouseButtonState::IsRightButtonDown, point.Properties().IsRightButtonPressed()); + return state; } unsigned int TermControl::GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point) diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 9cdb67e97..e2ee89b82 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -40,8 +40,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation float SnapDimensionToGrid(const bool widthOrHeight, const float dimension); #pragma region ICoreState - const size_t TaskbarState() const noexcept; - const size_t TaskbarProgress() const noexcept; + const uint64_t TaskbarState() const noexcept; + const uint64_t TaskbarProgress() const noexcept; hstring Title(); Windows::Foundation::IReference TabColor() noexcept; @@ -49,7 +49,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TerminalConnection::ConnectionState ConnectionState() const; - int ScrollOffset(); + int ScrollOffset() const; int ViewHeight() const; int BufferHeight() const; @@ -84,7 +84,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation ~TermControl(); Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); - ::Microsoft::Console::Types::IUiaData* GetUiaData() const; const Windows::UI::Xaml::Thickness GetPadding(); IControlSettings Settings() const; @@ -104,19 +103,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ReadOnly() const noexcept; void ToggleReadOnly(); - static ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point); + static Control::MouseButtonState GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point); static unsigned int GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point); + static Windows::UI::Xaml::Thickness ParseThicknessFromPadding(const hstring padding); // -------------------------------- WinRT Events --------------------------------- // clang-format off WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); - FORWARDED_TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs, _core, CopyToClipboard); - FORWARDED_TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs, _core, TitleChanged); - FORWARDED_TYPED_EVENT(TabColorChanged, IInspectable, IInspectable, _core, TabColorChanged); - FORWARDED_TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable, _core, TaskbarProgressChanged); - FORWARDED_TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable, _core, ConnectionStateChanged); - FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs, _interactivity, PasteFromClipboard); + PROJECTED_FORWARDED_TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs, _core, CopyToClipboard); + PROJECTED_FORWARDED_TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs, _core, TitleChanged); + PROJECTED_FORWARDED_TYPED_EVENT(TabColorChanged, IInspectable, IInspectable, _core, TabColorChanged); + PROJECTED_FORWARDED_TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable, _core, TaskbarProgressChanged); + PROJECTED_FORWARDED_TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable, _core, ConnectionStateChanged); + + PROJECTED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs, _interactivity, PasteFromClipboard); TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); TYPED_EVENT(RaiseNotice, IInspectable, Control::NoticeEventArgs); @@ -141,11 +142,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // IRenderEngine is accessed when ControlCore calls Renderer::TriggerTeardown. // (C++ class members are destroyed in reverse order.) // Further, the TermControlAutomationPeer must be destructed after _uiaEngine! - winrt::Windows::UI::Xaml::Automation::Peers::AutomationPeer _automationPeer{ nullptr }; - std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine; + Control::TermControlAutomationPeer _automationPeer{ nullptr }; + Control::ControlInteractivity _interactivity{ nullptr }; + Control::ControlCore _core{ nullptr }; - winrt::com_ptr _core; - winrt::com_ptr _interactivity; winrt::com_ptr _searchBox; IControlSettings _settings; @@ -153,8 +153,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool _focused{ false }; bool _initializedTerminal{ false }; - std::shared_ptr> _tsfTryRedrawCanvas; - std::shared_ptr> _updatePatternLocations; std::shared_ptr _playWarningBell; struct ScrollBarUpdate @@ -164,14 +162,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation double newMinimum; double newViewportSize; }; + std::shared_ptr> _updateScrollBar; + bool _isInternalScrollBarUpdate; - // Auto scroll occurs when user, while selecting, drags cursor outside viewport. View is then scrolled to 'follow' the cursor. + // Auto scroll occurs when user, while selecting, drags cursor outside + // viewport. View is then scrolled to 'follow' the cursor. double _autoScrollVelocity; std::optional _autoScrollingPointerPoint; Windows::UI::Xaml::DispatcherTimer _autoScrollTimer; std::optional _lastAutoScrollUpdateTime; + bool _pointerPressedInBounds{ false }; winrt::Windows::UI::Composition::ScalarKeyFrameAnimation _bellLightAnimation{ nullptr }; Windows::UI::Xaml::DispatcherTimer _bellLightTimer{ nullptr }; @@ -179,14 +181,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::optional _cursorTimer; std::optional _blinkTimer; - event_token _coreOutputEventToken; - winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; inline bool _IsClosing() const noexcept { +#ifndef NDEBUG // _closing isn't atomic and may only be accessed from the main thread. - assert(Dispatcher().HasThreadAccess()); + if (const auto dispatcher = Dispatcher()) + { + assert(dispatcher.HasThreadAccess()); + } +#endif return _closing; } @@ -231,7 +236,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _TerminalTabColorChanged(const std::optional color); void _ScrollPositionChanged(const IInspectable& sender, const Control::ScrollPositionChangedArgs& args); - void _CursorPositionChanged(const IInspectable& sender, const IInspectable& args); + winrt::fire_and_forget _CursorPositionChanged(const IInspectable& sender, const IInspectable& args); bool _CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); bool _ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); @@ -240,8 +245,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _TryStopAutoScroll(const uint32_t pointerId); void _UpdateAutoScroll(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); - static Windows::UI::Xaml::Thickness _ParseThicknessFromPadding(const hstring padding); - void _KeyHandler(Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e, const bool keyDown); ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() const; bool _TryHandleKeyBinding(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const; @@ -265,7 +268,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation const int fontHeight, const bool isInitialChange); winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); - void _coreReceivedOutput(const IInspectable& sender, const IInspectable& args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); }; diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp index 69af0750f..3cfae1cce 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp @@ -30,13 +30,43 @@ namespace XamlAutomation namespace winrt::Microsoft::Terminal::Control::implementation { - TermControlAutomationPeer::TermControlAutomationPeer(winrt::Microsoft::Terminal::Control::implementation::TermControl* owner) : + TermControlAutomationPeer::TermControlAutomationPeer(TermControl* owner, + const Core::Padding padding, + Control::InteractivityAutomationPeer impl) : TermControlAutomationPeerT(*owner), // pass owner to FrameworkElementAutomationPeer - _termControl{ owner } + _termControl{ owner }, + _contentAutomationPeer{ impl } { - THROW_IF_FAILED(::Microsoft::WRL::MakeAndInitialize<::Microsoft::Terminal::TermControlUiaProvider>(&_uiaProvider, _termControl->GetUiaData(), this)); + UpdateControlBounds(); + SetControlPadding(padding); + // Listen for UIA signalling events from the implementation. We need to + // be the one to actually raise these automation events, so they go + // through the UI tree correctly. + _contentAutomationPeer.SelectionChanged([this](auto&&, auto&&) { SignalSelectionChanged(); }); + _contentAutomationPeer.TextChanged([this](auto&&, auto&&) { SignalTextChanged(); }); + _contentAutomationPeer.CursorChanged([this](auto&&, auto&&) { SignalCursorChanged(); }); }; + // Method Description: + // - Inform the interactivity layer about the bounds of the control. + // IControlAccessibilityInfo needs to know this information, but it cannot + // ask us directly. + // Arguments: + // - + // Return Value: + // - + void TermControlAutomationPeer::UpdateControlBounds() + { + // FrameworkElementAutomationPeer has this great GetBoundingRectangle + // method that's seemingly impossible to recreate just from the + // UserControl itself. Weird. But we can use it handily here! + _contentAutomationPeer.SetControlBounds(GetBoundingRectangle()); + } + void TermControlAutomationPeer::SetControlPadding(const Core::Padding padding) + { + _contentAutomationPeer.SetControlPadding(padding); + } + // Method Description: // - Signals the ui automation client that the terminal's selection has changed and should be updated // Arguments: @@ -167,106 +197,36 @@ namespace winrt::Microsoft::Terminal::Control::implementation #pragma region ITextProvider com_array TermControlAutomationPeer::GetSelection() { - SAFEARRAY* pReturnVal; - THROW_IF_FAILED(_uiaProvider->GetSelection(&pReturnVal)); - return WrapArrayOfTextRangeProviders(pReturnVal); + return _contentAutomationPeer.GetSelection(); } com_array TermControlAutomationPeer::GetVisibleRanges() { - SAFEARRAY* pReturnVal; - THROW_IF_FAILED(_uiaProvider->GetVisibleRanges(&pReturnVal)); - return WrapArrayOfTextRangeProviders(pReturnVal); + return _contentAutomationPeer.GetVisibleRanges(); } XamlAutomation::ITextRangeProvider TermControlAutomationPeer::RangeFromChild(XamlAutomation::IRawElementProviderSimple childElement) { - UIA::ITextRangeProvider* returnVal; - // ScreenInfoUiaProvider doesn't actually use parameter, so just pass in nullptr - THROW_IF_FAILED(_uiaProvider->RangeFromChild(/* IRawElementProviderSimple */ nullptr, - &returnVal)); - - auto parentProvider = this->ProviderFromPeer(*this); - auto xutr = winrt::make_self(returnVal, parentProvider); - return xutr.as(); + return _contentAutomationPeer.RangeFromChild(childElement); } XamlAutomation::ITextRangeProvider TermControlAutomationPeer::RangeFromPoint(Windows::Foundation::Point screenLocation) { - UIA::ITextRangeProvider* returnVal; - THROW_IF_FAILED(_uiaProvider->RangeFromPoint({ screenLocation.X, screenLocation.Y }, &returnVal)); - - auto parentProvider = this->ProviderFromPeer(*this); - auto xutr = winrt::make_self(returnVal, parentProvider); - return xutr.as(); + return _contentAutomationPeer.RangeFromPoint(screenLocation); } XamlAutomation::ITextRangeProvider TermControlAutomationPeer::DocumentRange() { - UIA::ITextRangeProvider* returnVal; - THROW_IF_FAILED(_uiaProvider->get_DocumentRange(&returnVal)); - - auto parentProvider = this->ProviderFromPeer(*this); - auto xutr = winrt::make_self(returnVal, parentProvider); - return xutr.as(); + return _contentAutomationPeer.DocumentRange(); } XamlAutomation::SupportedTextSelection TermControlAutomationPeer::SupportedTextSelection() { - UIA::SupportedTextSelection returnVal; - THROW_IF_FAILED(_uiaProvider->get_SupportedTextSelection(&returnVal)); - return static_cast(returnVal); + return _contentAutomationPeer.SupportedTextSelection(); } #pragma endregion -#pragma region IControlAccessibilityInfo - COORD TermControlAutomationPeer::GetFontSize() const - { - return _termControl->GetFontSize(); - } - - RECT TermControlAutomationPeer::GetBounds() const - { - auto rect = GetBoundingRectangle(); - return { - gsl::narrow_cast(rect.X), - gsl::narrow_cast(rect.Y), - gsl::narrow_cast(rect.X + rect.Width), - gsl::narrow_cast(rect.Y + rect.Height) - }; - } - - HRESULT TermControlAutomationPeer::GetHostUiaProvider(IRawElementProviderSimple** provider) - { - RETURN_HR_IF(E_INVALIDARG, provider == nullptr); - *provider = nullptr; - - return S_OK; - } - - RECT TermControlAutomationPeer::GetPadding() const - { - auto padding = _termControl->GetPadding(); - return { - gsl::narrow_cast(padding.Left), - gsl::narrow_cast(padding.Top), - gsl::narrow_cast(padding.Right), - gsl::narrow_cast(padding.Bottom) - }; - } - - double TermControlAutomationPeer::GetScaleFactor() const - { - return DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); - } - - void TermControlAutomationPeer::ChangeViewport(const SMALL_RECT NewWindow) - { - _termControl->ScrollViewport(NewWindow.Top); - } -#pragma endregion - // Method Description: // - extracts the UiaTextRanges from the SAFEARRAY and converts them to Xaml ITextRangeProviders // Arguments: diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.h b/src/cascadia/TerminalControl/TermControlAutomationPeer.h index 46a900736..e5a2cc7ff 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.h +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.h @@ -20,13 +20,17 @@ Abstract: Author(s): - Carlos Zamora (CaZamor) 2019 + +Modifications: +- May 2021: Pulled the core logic of ITextProvider implementation into the + InteractivityAutomationPeer, to support tab tear out. --*/ #pragma once #include "TermControl.h" +#include "ControlInteractivity.h" #include "TermControlAutomationPeer.g.h" -#include #include "../types/TermControlUiaProvider.hpp" #include "../types/IUiaEventDispatcher.h" #include "../types/IControlAccessibilityInfo.h" @@ -35,11 +39,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation { struct TermControlAutomationPeer : public TermControlAutomationPeerT, - ::Microsoft::Console::Types::IUiaEventDispatcher, - ::Microsoft::Console::Types::IControlAccessibilityInfo + ::Microsoft::Console::Types::IUiaEventDispatcher { public: - TermControlAutomationPeer(Microsoft::Terminal::Control::implementation::TermControl* owner); + TermControlAutomationPeer(Microsoft::Terminal::Control::implementation::TermControl* owner, + const Core::Padding padding, + Control::InteractivityAutomationPeer implementation); + + void UpdateControlBounds(); + void SetControlPadding(const Core::Padding padding); #pragma region FrameworkElementAutomationPeer hstring GetClassNameCore() const; @@ -67,21 +75,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::UI::Xaml::Automation::Provider::ITextRangeProvider DocumentRange(); #pragma endregion -#pragma region IControlAccessibilityInfo Pattern - // Inherited via IControlAccessibilityInfo - virtual COORD GetFontSize() const override; - virtual RECT GetBounds() const override; - virtual RECT GetPadding() const override; - virtual double GetScaleFactor() const override; - virtual void ChangeViewport(SMALL_RECT NewWindow) override; - virtual HRESULT GetHostUiaProvider(IRawElementProviderSimple** provider) override; -#pragma endregion - - RECT GetBoundingRectWrapped(); - private: - ::Microsoft::WRL::ComPtr<::Microsoft::Terminal::TermControlUiaProvider> _uiaProvider; winrt::Microsoft::Terminal::Control::implementation::TermControl* _termControl; + Control::InteractivityAutomationPeer _contentAutomationPeer; + winrt::com_array WrapArrayOfTextRangeProviders(SAFEARRAY* textRanges); }; } diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.idl b/src/cascadia/TerminalControl/TermControlAutomationPeer.idl index 0d777b09a..4ca83f0d9 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.idl +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.idl @@ -9,5 +9,8 @@ namespace Microsoft.Terminal.Control Windows.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer, Windows.UI.Xaml.Automation.Provider.ITextProvider { + + void UpdateControlBounds(); + void SetControlPadding(Microsoft.Terminal.Core.Padding padding); } } diff --git a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj index 945dde889..3ce1d8a52 100644 --- a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj +++ b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj @@ -52,6 +52,9 @@ TermControlAutomationPeer.idl + + InteractivityAutomationPeer.idl + TSFInputControl.xaml @@ -91,6 +94,9 @@ TermControlAutomationPeer.idl + + InteractivityAutomationPeer.idl + @@ -112,6 +118,7 @@ TermControl.xaml + TSFInputControl.xaml diff --git a/src/cascadia/TerminalControl/pch.h b/src/cascadia/TerminalControl/pch.h index 730f71f58..bac6c29ca 100644 --- a/src/cascadia/TerminalControl/pch.h +++ b/src/cascadia/TerminalControl/pch.h @@ -57,6 +57,8 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalControlProvider); #include +#include + #include "til.h" #include "ThrottledFunc.h" diff --git a/src/cascadia/TerminalCore/ControlKeyStates.hpp b/src/cascadia/TerminalCore/ControlKeyStates.hpp index 9b88c71a3..9d09bb6e5 100644 --- a/src/cascadia/TerminalCore/ControlKeyStates.hpp +++ b/src/cascadia/TerminalCore/ControlKeyStates.hpp @@ -82,6 +82,20 @@ public: } #endif +#ifdef WINRT_Microsoft_Terminal_Core_H + constexpr ControlKeyStates(const winrt::Microsoft::Terminal::Core::ControlKeyStates& projKeyStates) : + ControlKeyStates(projKeyStates.Value) + { + } + + operator winrt::Microsoft::Terminal::Core::ControlKeyStates() const noexcept + { + winrt::Microsoft::Terminal::Core::ControlKeyStates ret; + ret.Value = _value; + return ret; + } +#endif + constexpr DWORD Value() const noexcept { return _value; diff --git a/src/cascadia/TerminalCore/ICoreAppearance.idl b/src/cascadia/TerminalCore/ICoreAppearance.idl index 30c91833c..5300ec674 100644 --- a/src/cascadia/TerminalCore/ICoreAppearance.idl +++ b/src/cascadia/TerminalCore/ICoreAppearance.idl @@ -24,11 +24,38 @@ namespace Microsoft.Terminal.Core UInt8 A; }; + // TerminalCore declares its own Color struct to avoid depending on + // Windows.UI. Windows.Foundation.Point also exists, but it's composed of + // floating-point coordinates, when we almost always need integer coordinates. + // It is supported by til::point for conversions in and out of WinRT land. + struct Point + { + Int32 X; + Int32 Y; + }; + + // Same thing here, but with padding. Can't use Windows.UI.Thickness, so + // we'll declare our own. + struct Padding { + Double Left; + Double Top; + Double Right; + Double Bottom; + }; + + // This is a projection of Microsoft::Terminal::Core::ControlKeyStates, + // for conversions in and out of WinRT land. + struct ControlKeyStates + { + UInt32 Value; + }; + declare { // Forward declare this parameterized specialization so that it lives // in TerminalCore instead of being flung to the winds of all IDL dependents. interface Windows.Foundation.IReference; + interface Windows.Foundation.IReference; } interface ICoreAppearance @@ -39,5 +66,6 @@ namespace Microsoft.Terminal.Core Microsoft.Terminal.Core.Color CursorColor; CursorStyle CursorShape; UInt32 CursorHeight; + Boolean IntenseIsBright; }; } diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 666068cb0..aba4d68de 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -52,7 +52,8 @@ Terminal::Terminal() : _selection{ std::nullopt }, _taskbarState{ 0 }, _taskbarProgress{ 0 }, - _trimBlockSelection{ false } + _trimBlockSelection{ false }, + _intenseIsBright{ true } { auto dispatch = std::make_unique(*this); auto engine = std::make_unique(std::move(dispatch)); @@ -173,6 +174,7 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance) til::color newBackgroundColor{ appearance.DefaultBackground() }; _defaultBg = newBackgroundColor.with_alpha(0); _defaultFg = appearance.DefaultForeground(); + _intenseIsBright = appearance.IntenseIsBright(); for (int i = 0; i < 16; i++) { @@ -230,16 +232,10 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance) } const auto dx = ::base::ClampSub(viewportSize.X, oldDimensions.X); - - const auto oldTop = _mutableViewport.Top(); - const short newBufferHeight = ::base::ClampAdd(viewportSize.Y, _scrollbackLines); COORD bufferSize{ viewportSize.X, newBufferHeight }; - // Save cursor's relative height versus the viewport - const short sCursorHeightInViewportBefore = ::base::ClampSub(_buffer->GetCursor().GetPosition().Y, _mutableViewport.Top()); - // This will be used to determine where the viewport should be in the new buffer. const short oldViewportTop = _mutableViewport.Top(); short newViewportTop = oldViewportTop; @@ -600,14 +596,6 @@ bool Terminal::SendKeyEvent(const WORD vkey, const auto isAltOnlyPressed = states.IsAltPressed() && !states.IsCtrlPressed(); - // DON'T manually handle Alt+Space - the system will use this to bring up - // the system menu for restore, min/maximize, size, move, close. - // (This doesn't apply to Ctrl+Alt+Space.) - if (isAltOnlyPressed && vkey == VK_SPACE) - { - return false; - } - // By default Windows treats Ctrl+Alt as an alias for AltGr. // When the altGrAliasing setting is set to false, this behaviour should be disabled. // @@ -678,13 +666,6 @@ bool Terminal::SendMouseEvent(const COORD viewportPos, const unsigned int uiButt // - false otherwise. bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) { - // DON'T manually handle Alt+Space - the system will use this to bring up - // the system menu for restore, min/maximize, size, move, close. - if (ch == L' ' && states.IsAltPressed() && !states.IsCtrlPressed()) - { - return false; - } - auto vkey = _TakeVirtualKeyFromLastKeyEvent(scanCode); if (vkey == 0 && scanCode != 0) { @@ -1010,6 +991,31 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition) _buffer->IncrementCircularBuffer(); proposedCursorPosition.Y--; rowsPushedOffTopOfBuffer++; + + // Update our selection too, so it doesn't move as the buffer is cycled + if (_selection) + { + // If the start of the selection is above 0, we can reduce both the start and end by 1 + if (_selection->start.Y > 0) + { + _selection->start.Y -= 1; + _selection->end.Y -= 1; + } + else + { + // The start of the selection is at 0, if the end is greater than 0, then only reduce the end + if (_selection->end.Y > 0) + { + _selection->start.X = 0; + _selection->end.Y -= 1; + } + else + { + // Both the start and end of the selection are at 0, clear the selection + _selection.reset(); + } + } + } } // manually erase our pattern intervals since the locations have changed now @@ -1070,7 +1076,12 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition) _buffer->GetRenderTarget().TriggerScroll(&delta); } - _NotifyTerminalCursorPositionChanged(); + // Firing the CursorPositionChanged event is very expensive so we try not to do that when + // the cursor does not need to be redrawn. + if (!cursor.IsDeferDrawing()) + { + _NotifyTerminalCursorPositionChanged(); + } } void Terminal::UserScrollViewport(const int viewTop) diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index aeabebc3b..27e26b7f2 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -282,6 +282,7 @@ private: bool _suppressApplicationTitle; bool _bracketedPasteMode; bool _trimBlockSelection; + bool _intenseIsBright; size_t _taskbarState; size_t _taskbarProgress; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 1fc36389d..83c898e47 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -16,7 +16,7 @@ try _WriteBuffer(stringView); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::ExecuteChar(wchar_t wch) noexcept try @@ -24,7 +24,7 @@ try _WriteBuffer({ &wch, 1 }); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() TextAttribute Terminal::GetTextAttributes() const noexcept { @@ -54,7 +54,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() COORD Terminal::GetCursorPosition() noexcept { @@ -75,7 +75,7 @@ try _buffer->GetCursor().SetColor(color); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Moves the cursor down one line, and possibly also to the leftmost column. @@ -101,7 +101,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - deletes count characters starting from the cursor's current position @@ -150,7 +150,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Inserts count spaces starting from the cursor's current position, moving over the existing text @@ -205,7 +205,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::EraseCharacters(const size_t numChars) noexcept try @@ -218,7 +218,7 @@ try _buffer->Write(eraseIter, absoluteCursorPos); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method description: // - erases a line of text, either from @@ -264,7 +264,7 @@ try _buffer->Write(eraseIter, startPos, false); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method description: // - erases text in the buffer in two ways depending on erase type @@ -348,7 +348,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::WarningBell() noexcept try @@ -356,7 +356,7 @@ try _pfnWarningBell(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::SetWindowTitle(std::wstring_view title) noexcept try @@ -368,7 +368,7 @@ try } return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Updates the value in the colortable at index tableIndex to the new color @@ -387,7 +387,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Sets the cursor style to the given style. @@ -457,7 +457,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Updates the default background color from a COLORREF, format 0x00BBGGRR. @@ -475,7 +475,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() til::color Terminal::GetDefaultBackground() const noexcept { @@ -509,7 +509,7 @@ try _buffer->GetRenderTarget().TriggerRedrawAll(); return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() bool Terminal::EnableVT200MouseMode(const bool enabled) noexcept { @@ -591,7 +591,7 @@ try return true; } -CATCH_LOG_RETURN_FALSE() +CATCH_RETURN_FALSE() // Method Description: // - Updates the buffer's current text attributes to start a hyperlink diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 5566e7c8d..84b00eadf 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -45,11 +45,13 @@ const TextAttribute Terminal::GetDefaultBrushColors() noexcept std::pair Terminal::GetAttributeColors(const TextAttribute& attr) const noexcept { _blinkingState.RecordBlinkingUsage(attr); - auto colors = attr.CalculateRgbColors({ _colorTable.data(), _colorTable.size() }, - _defaultFg, - _defaultBg, - _screenReversed, - _blinkingState.IsBlinkingFaint()); + auto colors = attr.CalculateRgbColors( + _colorTable, + _defaultFg, + _defaultBg, + _screenReversed, + _blinkingState.IsBlinkingFaint(), + _intenseIsBright); colors.first |= 0xff000000; // We only care about alpha for the default BG (which enables acrylic) // If the bg isn't the default bg color, or reverse video is enabled, make it fully opaque. diff --git a/src/cascadia/TerminalSettingsEditor/Actions.cpp b/src/cascadia/TerminalSettingsEditor/Actions.cpp index ffd2ae343..4f65c8cba 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.cpp +++ b/src/cascadia/TerminalSettingsEditor/Actions.cpp @@ -25,8 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation KeyBindingViewModel(nullptr, availableActions.First().Current(), availableActions) {} KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector& availableActions) : - _Keys{ keys }, - _KeyChordText{ Model::KeyChordSerialization::ToString(keys) }, + _CurrentKeys{ keys }, + _KeyChordText{ KeyChordSerialization::ToString(keys) }, _CurrentAction{ actionName }, _ProposedAction{ box_value(actionName) }, _AvailableActions{ availableActions } @@ -36,9 +36,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // unique view model members. PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) { const auto viewModelProperty{ args.PropertyName() }; - if (viewModelProperty == L"Keys") + if (viewModelProperty == L"CurrentKeys") { - _KeyChordText = Model::KeyChordSerialization::ToString(_Keys); + _KeyChordText = KeyChordSerialization::ToString(_CurrentKeys); _NotifyChanges(L"KeyChordText"); } else if (viewModelProperty == L"IsContainerFocused" || @@ -75,8 +75,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // if we're in edit mode, // - pre-populate the text box with the current keys // - reset the combo box with the current action - ProposedKeys(KeyChordText()); - ProposedAction(box_value(CurrentAction())); + ProposedKeys(_CurrentKeys); + ProposedAction(box_value(_CurrentAction)); } } @@ -85,35 +85,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation AttemptAcceptChanges(_ProposedKeys); } - void KeyBindingViewModel::AttemptAcceptChanges(hstring newKeyChordText) + void KeyBindingViewModel::AttemptAcceptChanges(const Control::KeyChord newKeys) { - try - { - // empty string --> don't accept changes - if (newKeyChordText.empty()) - { - return; - } - - // ModifyKeyBindingEventArgs - const auto args{ make_self(_Keys, // OldKeys - KeyChordSerialization::FromString(newKeyChordText), // NewKeys: Attempt to convert the provided key chord text - _IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction - unbox_value(_ProposedAction)) }; // - _ModifyKeyBindingRequestedHandlers(*this, *args); - } - catch (hresult_invalid_argument) - { - // Converting the text into a key chord failed. - // Don't accept the changes. - // TODO GH #6900: - // This is tricky. I still haven't found a way to reference the - // key chord text box. It's hidden behind the data template. - // Ideally, some kind of notification would alert the user, but - // to make it look nice, we need it to somehow target the text box. - // Alternatively, we want a full key chord editor/listener. - // If we implement that, we won't need this validation or error message. - } + const auto args{ make_self(_CurrentKeys, // OldKeys + newKeys, // NewKeys + _IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction + unbox_value(_ProposedAction)) }; // NewAction + _ModifyKeyBindingRequestedHandlers(*this, *args); } void KeyBindingViewModel::CancelChanges() @@ -179,34 +157,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList)); } - void Actions::KeyChordEditor_KeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) - { - const auto& senderTB{ sender.as() }; - const auto& kbdVM{ senderTB.DataContext().as() }; - if (e.OriginalKey() == VirtualKey::Enter) - { - // Fun fact: this is happening _before_ "_ProposedKeys" gets updated - // with the two-way data binding. So we need to directly extract the text - // and tell the view model to update itself. - get_self(kbdVM)->AttemptAcceptChanges(senderTB.Text()); - - // For an unknown reason, when 'AcceptChangesFlyout' is set in the code above, - // the flyout isn't shown, forcing the 'Enter' key to do nothing. - // To get around this, detect if the flyout was set, and display it - // on the text box. - if (kbdVM.AcceptChangesFlyout() != nullptr) - { - kbdVM.AcceptChangesFlyout().ShowAt(senderTB); - } - e.Handled(true); - } - else if (e.OriginalKey() == VirtualKey::Escape) - { - kbdVM.CancelChanges(); - e.Handled(true); - } - } - void Actions::AddNew_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/) { // Create the new key binding and register all of the event handlers. @@ -314,7 +264,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // update view model auto senderVMImpl{ get_self(senderVM) }; - senderVMImpl->Keys(args.NewKeys()); + senderVMImpl->CurrentKeys(args.NewKeys()); } // If the action was changed, @@ -418,7 +368,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i) { const auto kbdVM{ get_self(_KeyBindingList.GetAt(i)) }; - const auto& otherKeys{ kbdVM->Keys() }; + const auto& otherKeys{ kbdVM->CurrentKeys() }; if (keys.Modifiers() == otherKeys.Modifiers() && keys.Vkey() == otherKeys.Vkey()) { return i; diff --git a/src/cascadia/TerminalSettingsEditor/Actions.h b/src/cascadia/TerminalSettingsEditor/Actions.h index 14d060a8a..97ae3d58b 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.h +++ b/src/cascadia/TerminalSettingsEditor/Actions.h @@ -60,9 +60,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void ToggleEditMode(); void DisableEditMode() { IsInEditMode(false); } void AttemptAcceptChanges(); - void AttemptAcceptChanges(hstring newKeyChordText); + void AttemptAcceptChanges(const Control::KeyChord newKeys); void CancelChanges(); - void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _Keys); } + void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _CurrentKeys); } // ProposedAction: the entry selected by the combo box; may disagree with the settings model. // CurrentAction: the combo box item that maps to the settings model value. @@ -77,10 +77,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction); WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, AvailableActions, nullptr); - // ProposedKeys: the text shown in the text box; may disagree with the settings model. - // Keys: the key chord bound in the settings model. - VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProposedKeys); - VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, Keys, nullptr); + // ProposedKeys: the keys proposed by the control; may disagree with the settings model. + // CurrentKeys: the key chord bound in the settings model. + VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys); + VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, CurrentKeys, nullptr); VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false); VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsNewlyAdded, false); @@ -107,14 +107,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_PROPERTY(Model::CascadiaSettings, Settings, nullptr) }; - struct Actions : ActionsT + struct Actions : public HasScrollViewer, ActionsT { public: Actions(); void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); - void KeyChordEditor_KeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); void AddNew_Click(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); diff --git a/src/cascadia/TerminalSettingsEditor/Actions.idl b/src/cascadia/TerminalSettingsEditor/Actions.idl index 98d8ee3e6..31c8570cc 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.idl +++ b/src/cascadia/TerminalSettingsEditor/Actions.idl @@ -23,7 +23,7 @@ namespace Microsoft.Terminal.Settings.Editor Boolean ShowEditButton { get; }; Boolean IsInEditMode { get; }; Boolean IsNewlyAdded { get; }; - String ProposedKeys; + Microsoft.Terminal.Control.KeyChord ProposedKeys; Object ProposedAction; Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout; String EditButtonName { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/Actions.xaml b/src/cascadia/TerminalSettingsEditor/Actions.xaml index d1d75c8a0..35a59cc8f 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Actions.xaml @@ -133,13 +133,9 @@ 32 15 @@ -162,7 +158,6 @@ - + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(IsInEditMode), Mode=OneWay}" /> - + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(IsInEditMode), Mode=OneWay}"> - - + + - - - - - - + @@ -106,7 +101,7 @@ + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(IsRenaming), Mode=OneWay}"> (value))); - } - - Foundation::IInspectable ColorToBrushConverter::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/ColorToBrushConverter.h b/src/cascadia/TerminalSettingsEditor/ColorToBrushConverter.h deleted file mode 100644 index 94c20db6d..000000000 --- a/src/cascadia/TerminalSettingsEditor/ColorToBrushConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "ColorToBrushConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct ColorToBrushConverter : ColorToBrushConverterT - { - ColorToBrushConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(ColorToBrushConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.cpp b/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.cpp deleted file mode 100644 index 1d9379ab3..000000000 --- a/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "ColorToHexConverter.h" -#include "ColorToHexConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable ColorToHexConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - til::color color{ winrt::unbox_value(value) }; - auto hex = winrt::to_hstring(color.ToHexString().data()); - return winrt::box_value(hex); - } - - Foundation::IInspectable ColorToHexConverter::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/ColorToHexConverter.h b/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.h deleted file mode 100644 index 53e2963df..000000000 --- a/src/cascadia/TerminalSettingsEditor/ColorToHexConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "ColorToHexConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct ColorToHexConverter : ColorToHexConverterT - { - ColorToHexConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(ColorToHexConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/Converters.cpp b/src/cascadia/TerminalSettingsEditor/Converters.cpp new file mode 100644 index 000000000..d9cff642e --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Converters.cpp @@ -0,0 +1,106 @@ +#include "pch.h" +#include "Converters.h" +#if __has_include("Converters.g.cpp") +#include "Converters.g.cpp" +#endif + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + winrt::hstring Converters::AppendPercentageSign(double value) + { + const auto number{ value }; + return to_hstring((int)number) + L"%"; + } + + winrt::Windows::UI::Xaml::Media::SolidColorBrush Converters::ColorToBrush(winrt::Windows::UI::Color color) + { + return Windows::UI::Xaml::Media::SolidColorBrush(color); + } + + winrt::Windows::UI::Text::FontWeight Converters::DoubleToFontWeight(double value) + { + return winrt::Windows::UI::Text::FontWeight{ base::ClampedNumeric(value) }; + } + + double Converters::FontWeightToDouble(winrt::Windows::UI::Text::FontWeight fontWeight) + { + return fontWeight.Weight; + } + + bool Converters::InvertBoolean(bool value) + { + return !value; + } + + winrt::Windows::UI::Xaml::Visibility Converters::InvertedBooleanToVisibility(bool value) + { + return value ? winrt::Windows::UI::Xaml::Visibility::Collapsed : winrt::Windows::UI::Xaml::Visibility::Visible; + } + + winrt::Windows::UI::Color Converters::LightenColor(winrt::Windows::UI::Color color) + { + color.A = 128; // halfway transparent + return color; + } + + double Converters::MaxValueFromPaddingString(winrt::hstring paddingString) + { + const wchar_t singleCharDelim = L','; + std::wstringstream tokenStream(paddingString.c_str()); + std::wstring token; + double maxVal = 0; + size_t* idx = nullptr; + + // Get padding values till we run out of delimiter separated values in the stream + // Non-numeral values detected will default to 0 + // std::getline will not throw exception unless flags are set on the wstringstream + // std::stod will throw invalid_argument exception if the input is an invalid double value + // std::stod will throw out_of_range exception if the input value is more than DBL_MAX + try + { + while (std::getline(tokenStream, token, singleCharDelim)) + { + // std::stod internally calls wcstod which handles whitespace prefix (which is ignored) + // & stops the scan when first char outside the range of radix is encountered + // We'll be permissive till the extent that stod function allows us to be by default + // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail + const auto curVal = std::stod(token, idx); + if (curVal > maxVal) + { + maxVal = curVal; + } + } + } + catch (...) + { + // If something goes wrong, even if due to a single bad padding value, we'll return default 0 padding + maxVal = 0; + LOG_CAUGHT_EXCEPTION(); + } + + return maxVal; + } + + int Converters::PercentageToPercentageValue(double value) + { + return base::ClampMul(value, 100u); + } + + double Converters::PercentageValueToPercentage(double value) + { + return base::ClampDiv(value, 100); + } + + bool Converters::StringsAreNotEqual(winrt::hstring expected, winrt::hstring actual) + { + return expected != actual; + } + winrt::Windows::UI::Xaml::Visibility Converters::StringNotEmptyToVisibility(winrt::hstring value) + { + return value.empty() ? winrt::Windows::UI::Xaml::Visibility::Collapsed : winrt::Windows::UI::Xaml::Visibility::Visible; + } + winrt::hstring Converters::StringFallBackToEmptyString(winrt::hstring expected, winrt::hstring actual) + { + return expected == actual ? expected : L""; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/Converters.h b/src/cascadia/TerminalSettingsEditor/Converters.h new file mode 100644 index 000000000..d7f99cbf2 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Converters.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "Converters.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct Converters : ConvertersT + { + static winrt::hstring AppendPercentageSign(double value); + static winrt::Windows::UI::Text::FontWeight DoubleToFontWeight(double value); + static winrt::Windows::UI::Xaml::Media::SolidColorBrush ColorToBrush(winrt::Windows::UI::Color color); + static double FontWeightToDouble(winrt::Windows::UI::Text::FontWeight fontWeight); + static bool InvertBoolean(bool value); + static winrt::Windows::UI::Xaml::Visibility InvertedBooleanToVisibility(bool value); + static winrt::Windows::UI::Color LightenColor(winrt::Windows::UI::Color color); + static double MaxValueFromPaddingString(winrt::hstring paddingString); + static int PercentageToPercentageValue(double value); + static double PercentageValueToPercentage(double value); + static bool StringsAreNotEqual(winrt::hstring expected, winrt::hstring actual); + static winrt::Windows::UI::Xaml::Visibility StringNotEmptyToVisibility(winrt::hstring value); + static winrt::hstring StringFallBackToEmptyString(winrt::hstring expected, winrt::hstring actual); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + struct Converters : ConvertersT + { + }; +} diff --git a/src/cascadia/TerminalSettingsEditor/Converters.idl b/src/cascadia/TerminalSettingsEditor/Converters.idl index 89eb05791..a5b79d5fc 100644 --- a/src/cascadia/TerminalSettingsEditor/Converters.idl +++ b/src/cascadia/TerminalSettingsEditor/Converters.idl @@ -4,63 +4,22 @@ namespace Microsoft.Terminal.Settings.Editor { - runtimeclass ColorLightenConverter : [default] Windows.UI.Xaml.Data.IValueConverter + [bindable] + [default_interface] static runtimeclass Converters { - ColorLightenConverter(); - }; - runtimeclass FontWeightConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - FontWeightConverter(); - }; - - runtimeclass InvertedBooleanConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - InvertedBooleanConverter(); - }; - - runtimeclass InvertedBooleanToVisibilityConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - InvertedBooleanToVisibilityConverter(); - }; - - runtimeclass ColorToBrushConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - ColorToBrushConverter(); - }; - - runtimeclass ColorToHexConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - ColorToHexConverter(); - }; - - runtimeclass PercentageConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - PercentageConverter(); - }; - - runtimeclass PercentageSignConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - PercentageSignConverter(); - }; - - runtimeclass StringIsEmptyConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - StringIsEmptyConverter(); - }; - - runtimeclass PaddingConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - PaddingConverter(); - }; - - runtimeclass StringIsNotDesktopConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - StringIsNotDesktopConverter(); - }; - - runtimeclass DesktopWallpaperToEmptyStringConverter : [default] Windows.UI.Xaml.Data.IValueConverter - { - DesktopWallpaperToEmptyStringConverter(); - }; + static String AppendPercentageSign(Double value); + static Windows.UI.Text.FontWeight DoubleToFontWeight(Double value); + static Windows.UI.Xaml.Media.SolidColorBrush ColorToBrush(Windows.UI.Color color); + static Double FontWeightToDouble(Windows.UI.Text.FontWeight fontWeight); + static Boolean InvertBoolean(Boolean value); + static Windows.UI.Xaml.Visibility InvertedBooleanToVisibility(Boolean value); + static Windows.UI.Color LightenColor(Windows.UI.Color color); + static Double MaxValueFromPaddingString(String paddingString); + static Int32 PercentageToPercentageValue(Double value); + static Double PercentageValueToPercentage(Double value); + static Boolean StringsAreNotEqual(String expected, String actual); + static Windows.UI.Xaml.Visibility StringNotEmptyToVisibility(String value); + static String StringFallBackToEmptyString(String expected, String actual); + } } diff --git a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.cpp b/src/cascadia/TerminalSettingsEditor/FontWeightConverter.cpp deleted file mode 100644 index 5b39eded1..000000000 --- a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "FontWeightConverter.h" -#include "FontWeightConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; -using namespace winrt::Windows::UI::Text; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable FontWeightConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - const auto weight{ winrt::unbox_value(value) }; - return winrt::box_value(weight.Weight); - } - - Foundation::IInspectable FontWeightConverter::ConvertBack(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - const auto sliderVal{ winrt::unbox_value(value) }; - FontWeight weight{ base::ClampedNumeric(sliderVal) }; - return winrt::box_value(weight); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.h b/src/cascadia/TerminalSettingsEditor/FontWeightConverter.h deleted file mode 100644 index 249e7987d..000000000 --- a/src/cascadia/TerminalSettingsEditor/FontWeightConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "FontWeightConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct FontWeightConverter : FontWeightConverterT - { - FontWeightConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(FontWeightConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h index c46053953..c566b953a 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h @@ -18,7 +18,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_PROPERTY(Model::GlobalAppSettings, Globals, nullptr) }; - struct GlobalAppearance : GlobalAppearanceT + struct GlobalAppearance : public HasScrollViewer, GlobalAppearanceT { public: GlobalAppearance(); diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml index ae030da7c..5ecb1169f 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml @@ -21,12 +21,10 @@ x:DataType="local:EnumEntry"> - - - + + + + + + @@ -79,7 +82,7 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/Interaction.h b/src/cascadia/TerminalSettingsEditor/Interaction.h index ac93655de..bf6dc598b 100644 --- a/src/cascadia/TerminalSettingsEditor/Interaction.h +++ b/src/cascadia/TerminalSettingsEditor/Interaction.h @@ -18,7 +18,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_PROPERTY(Model::GlobalAppSettings, Globals, nullptr) }; - struct Interaction : InteractionT + struct Interaction : public HasScrollViewer, InteractionT { Interaction(); diff --git a/src/cascadia/TerminalSettingsEditor/Interaction.xaml b/src/cascadia/TerminalSettingsEditor/Interaction.xaml index 35af1fbfc..72f8d21c2 100644 --- a/src/cascadia/TerminalSettingsEditor/Interaction.xaml +++ b/src/cascadia/TerminalSettingsEditor/Interaction.xaml @@ -24,7 +24,7 @@ - + (value)); - } - - Foundation::IInspectable InvertedBooleanConverter::ConvertBack(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - return winrt::box_value(!winrt::unbox_value(value)); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.h b/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.h deleted file mode 100644 index 9da6768fe..000000000 --- a/src/cascadia/TerminalSettingsEditor/InvertedBooleanConverter.h +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "InvertedBooleanConverter.g.h" -#include "../inc/cppwinrt_utils.h" - -DECLARE_CONVERTER(winrt::Microsoft::Terminal::Settings::Editor, InvertedBooleanConverter); diff --git a/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.cpp b/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.cpp deleted file mode 100644 index fd3c9e6fc..000000000 --- a/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "InvertedBooleanToVisibilityConverter.h" -#include "InvertedBooleanToVisibilityConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable InvertedBooleanToVisibilityConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - return winrt::box_value(winrt::unbox_value(value) ? Visibility::Collapsed : Visibility::Visible); - } - - Foundation::IInspectable InvertedBooleanToVisibilityConverter::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/InvertedBooleanToVisibilityConverter.h b/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.h deleted file mode 100644 index 90c6bbb74..000000000 --- a/src/cascadia/TerminalSettingsEditor/InvertedBooleanToVisibilityConverter.h +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "InvertedBooleanToVisibilityConverter.g.h" -#include "../inc/cppwinrt_utils.h" - -DECLARE_CONVERTER(winrt::Microsoft::Terminal::Settings::Editor, InvertedBooleanToVisibilityConverter); diff --git a/src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp b/src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp new file mode 100644 index 000000000..9fc417ce3 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/KeyChordListener.cpp @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "KeyChordListener.h" +#include "KeyChordListener.g.cpp" +#include "LibraryResources.h" + +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Xaml::Data; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::System; +using namespace winrt::Windows::UI::Xaml::Input; + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + DependencyProperty KeyChordListener::_KeysProperty{ nullptr }; + + // The ModifierKeys have been sorted by value. + // Not just binary search, but also your CPU likes sorted data. + static constexpr std::array ModifierKeys{ + VirtualKey::Shift, + VirtualKey::Control, + VirtualKey::Menu, + VirtualKey::LeftWindows, + VirtualKey::RightWindows, + VirtualKey::LeftShift, + VirtualKey::LeftControl, + VirtualKey::RightControl, + VirtualKey::LeftMenu, + VirtualKey::RightMenu + }; + + static VirtualKeyModifiers _GetModifiers() + { + const auto window{ CoreWindow::GetForCurrentThread() }; + + VirtualKeyModifiers flags = VirtualKeyModifiers::None; + for (const auto mod : ModifierKeys) + { + const auto state = window.GetKeyState(mod); + const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); + + if (isDown) + { + switch (mod) + { + case VirtualKey::Control: + case VirtualKey::LeftControl: + case VirtualKey::RightControl: + flags |= VirtualKeyModifiers::Control; + break; + case VirtualKey::Menu: + case VirtualKey::LeftMenu: + case VirtualKey::RightMenu: + flags |= VirtualKeyModifiers::Menu; + break; + case VirtualKey::Shift: + case VirtualKey::LeftShift: + flags |= VirtualKeyModifiers::Shift; + break; + case VirtualKey::LeftWindows: + case VirtualKey::RightWindows: + flags |= VirtualKeyModifiers::Windows; + break; + } + } + } + return flags; + } + + KeyChordListener::KeyChordListener() + { + InitializeComponent(); + _InitializeProperties(); + } + + void KeyChordListener::_InitializeProperties() + { + // Initialize any KeyChordListener dependency properties here. + // This performs a lazy load on these properties, instead of + // initializing them when the DLL loads. + if (!_KeysProperty) + { + _KeysProperty = + DependencyProperty::Register( + L"Keys", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr, PropertyChangedCallback{ &KeyChordListener::_OnKeysChanged } }); + } + } + + void KeyChordListener::_OnKeysChanged(DependencyObject const& d, DependencyPropertyChangedEventArgs const& e) + { + if (auto control{ d.try_as() }) + { + auto controlImpl{ get_self(control) }; + TextBox tb{ controlImpl->FindName(L"KeyChordTextBox").as() }; + tb.Text(Model::KeyChordSerialization::ToString(unbox_value(e.NewValue()))); + if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(tb) }) + { + automationPeer.RaiseNotificationEvent( + Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::MostRecent, + tb.Text(), + L"KeyChordListenerText"); + } + } + } + + void KeyChordListener::KeyChordTextBox_KeyDown(IInspectable const& /*sender*/, KeyRoutedEventArgs const& e) + { + const auto key{ e.OriginalKey() }; + for (const auto mod : ModifierKeys) + { + if (key == mod) + { + // Ignore modifier keys + return; + } + } + + const auto modifiers{ _GetModifiers() }; + if (key == VirtualKey::Tab && (modifiers == VirtualKeyModifiers::None || modifiers == VirtualKeyModifiers::Shift)) + { + // [Shift]+[Tab] && [Tab] are needed for keyboard navigation + return; + } + + // Permitted key events are used to update _Keys + Keys({ modifiers, static_cast(key), 0 }); + e.Handled(true); + } +} diff --git a/src/cascadia/TerminalSettingsEditor/KeyChordListener.h b/src/cascadia/TerminalSettingsEditor/KeyChordListener.h new file mode 100644 index 000000000..526aa76dd --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/KeyChordListener.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "KeyChordListener.g.h" +#include "Utils.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct KeyChordListener : KeyChordListenerT + { + public: + KeyChordListener(); + + void KeyChordTextBox_KeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + + DEPENDENCY_PROPERTY(Control::KeyChord, Keys); + + private: + static void _InitializeProperties(); + static void _OnKeysChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(KeyChordListener); +} diff --git a/src/cascadia/TerminalSettingsEditor/KeyChordListener.idl b/src/cascadia/TerminalSettingsEditor/KeyChordListener.idl new file mode 100644 index 000000000..122efe08f --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/KeyChordListener.idl @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings.Editor +{ + [default_interface] runtimeclass KeyChordListener : Windows.UI.Xaml.Controls.UserControl + { + KeyChordListener(); + + Microsoft.Terminal.Control.KeyChord Keys; + static Windows.UI.Xaml.DependencyProperty KeysProperty { get; }; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/KeyChordListener.xaml b/src/cascadia/TerminalSettingsEditor/KeyChordListener.xaml new file mode 100644 index 000000000..9068ede48 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/KeyChordListener.xaml @@ -0,0 +1,17 @@ + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Launch.cpp b/src/cascadia/TerminalSettingsEditor/Launch.cpp index 99b15cf73..b881b4e00 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.cpp +++ b/src/cascadia/TerminalSettingsEditor/Launch.cpp @@ -47,4 +47,25 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto profile{ winrt::unbox_value(value) }; _State.Settings().GlobalSettings().DefaultProfile(profile.Guid()); } + + winrt::Windows::Foundation::Collections::IObservableVector Launch::DefaultProfiles() const + { + const auto allProfiles = _State.Settings().AllProfiles(); + + std::vector profiles; + profiles.reserve(allProfiles.Size()); + + // Remove profiles from the selection which have been explicitly deleted. + // We do want to show hidden profiles though, as they are just hidden + // from menus, but still work as the startup profile for instance. + for (const auto& profile : allProfiles) + { + if (!profile.Deleted()) + { + profiles.emplace_back(profile); + } + } + + return winrt::single_threaded_observable_vector(std::move(profiles)); + } } diff --git a/src/cascadia/TerminalSettingsEditor/Launch.h b/src/cascadia/TerminalSettingsEditor/Launch.h index 8219939e0..82fed6e41 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.h +++ b/src/cascadia/TerminalSettingsEditor/Launch.h @@ -18,7 +18,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_PROPERTY(Model::CascadiaSettings, Settings, nullptr) }; - struct Launch : LaunchT + struct Launch : public HasScrollViewer, LaunchT { public: Launch(); @@ -27,6 +27,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation IInspectable CurrentDefaultProfile(); void CurrentDefaultProfile(const IInspectable& value); + winrt::Windows::Foundation::Collections::IObservableVector DefaultProfiles() const; WINRT_PROPERTY(Editor::LaunchPageNavigationState, State, nullptr); diff --git a/src/cascadia/TerminalSettingsEditor/Launch.idl b/src/cascadia/TerminalSettingsEditor/Launch.idl index d59228bad..a5b132eb2 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.idl +++ b/src/cascadia/TerminalSettingsEditor/Launch.idl @@ -16,6 +16,9 @@ namespace Microsoft.Terminal.Settings.Editor LaunchPageNavigationState State { get; }; IInspectable CurrentDefaultProfile; + // I wish this was a IObservableVector, but: + // https://github.com/microsoft/microsoft-ui-xaml/issues/5395 + IObservableVector DefaultProfiles { get; }; IInspectable CurrentLaunchMode; IObservableVector LaunchModeList { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/Launch.xaml b/src/cascadia/TerminalSettingsEditor/Launch.xaml index e342369ba..caac364f4 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.xaml +++ b/src/cascadia/TerminalSettingsEditor/Launch.xaml @@ -33,14 +33,14 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 71a775cb0..414bd4f0d 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -387,14 +387,19 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void MainPage::_InitializeProfilesList() { + const auto menuItems = SettingsNav().MenuItems(); + // Manually create a NavigationViewItem for each profile // and keep a reference to them in a map so that we // can easily modify the correct one when the associated // profile changes. for (const auto& profile : _settingsClone.AllProfiles()) { - auto navItem = _CreateProfileNavViewItem(_viewModelForProfile(profile, _settingsClone)); - SettingsNav().MenuItems().Append(navItem); + if (!profile.Deleted()) + { + auto navItem = _CreateProfileNavViewItem(_viewModelForProfile(profile, _settingsClone)); + menuItems.Append(navItem); + } } // Top off (the end of the nav view) with the Add Profile item @@ -407,7 +412,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation icon.Glyph(L"\xE710"); addProfileItem.Icon(icon); - SettingsNav().MenuItems().Append(addProfileItem); + menuItems.Append(addProfileItem); } void MainPage::_CreateAndNavigateToNewProfile(const uint32_t index, const Model::Profile& profile) diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index 23f66358d..9576102a1 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -116,11 +116,22 @@ - - - + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj index 9dea36236..35d696d2c 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj @@ -42,38 +42,9 @@ AddProfile.xaml - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - + Converters.idl + Code EnumEntry.idl @@ -88,6 +59,9 @@ Interaction.xaml + + KeyChordListener.xaml + Launch.xaml @@ -135,6 +109,9 @@ Designer + + Designer + Designer @@ -165,38 +142,9 @@ AddProfile.xaml - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - - Converters.idl - - + Converters.idl + Code GlobalAppearance.xaml @@ -208,6 +156,9 @@ Interaction.xaml + + KeyChordListener.xaml + Launch.xaml @@ -260,6 +211,10 @@ ColorSchemes.xaml Code + + KeyChordListener.xaml + Code + Launch.xaml Code @@ -326,7 +281,6 @@ true false - false @@ -358,12 +312,12 @@ - + 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}. - + - + \ 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 1541f175e..3b0b9186e 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters @@ -10,16 +10,11 @@ - - Converters - - - Converters - + @@ -44,10 +39,8 @@ - - - - {00f725c8-41b4-40a8-995e-8ee2e49a4a4c} - + + + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsEditor/PaddingConverter.cpp b/src/cascadia/TerminalSettingsEditor/PaddingConverter.cpp deleted file mode 100644 index 6c0e3472c..000000000 --- a/src/cascadia/TerminalSettingsEditor/PaddingConverter.cpp +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "PaddingConverter.h" -#include "PaddingConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; -using namespace winrt::Windows::UI::Text; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable PaddingConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - const auto& padding = winrt::unbox_value(value); - - const wchar_t singleCharDelim = L','; - std::wstringstream tokenStream(padding.c_str()); - std::wstring token; - double maxVal = 0; - size_t* idx = nullptr; - - // Get padding values till we run out of delimiter separated values in the stream - // Non-numeral values detected will default to 0 - // std::getline will not throw exception unless flags are set on the wstringstream - // std::stod will throw invalid_argument exception if the input is an invalid double value - // std::stod will throw out_of_range exception if the input value is more than DBL_MAX - try - { - while (std::getline(tokenStream, token, singleCharDelim)) - { - // std::stod internally calls wcstod which handles whitespace prefix (which is ignored) - // & stops the scan when first char outside the range of radix is encountered - // We'll be permissive till the extent that stod function allows us to be by default - // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail - const auto curVal = std::stod(token, idx); - if (curVal > maxVal) - { - maxVal = curVal; - } - } - } - catch (...) - { - // If something goes wrong, even if due to a single bad padding value, we'll return default 0 padding - maxVal = 0; - LOG_CAUGHT_EXCEPTION(); - } - - return winrt::box_value(maxVal); - } - - Foundation::IInspectable PaddingConverter::ConvertBack(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /*parameter*/, - hstring const& /* language */) - { - const auto padding{ winrt::unbox_value(value) }; - return winrt::box_value(winrt::to_hstring(padding)); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/PaddingConverter.h b/src/cascadia/TerminalSettingsEditor/PaddingConverter.h deleted file mode 100644 index c56dd6d9f..000000000 --- a/src/cascadia/TerminalSettingsEditor/PaddingConverter.h +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "PaddingConverter.g.h" -#include "../inc/cppwinrt_utils.h" - -DECLARE_CONVERTER(winrt::Microsoft::Terminal::Settings::Editor, PaddingConverter); diff --git a/src/cascadia/TerminalSettingsEditor/PercentageConverter.cpp b/src/cascadia/TerminalSettingsEditor/PercentageConverter.cpp deleted file mode 100644 index b414532bf..000000000 --- a/src/cascadia/TerminalSettingsEditor/PercentageConverter.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "PercentageConverter.h" -#include "PercentageConverter.g.cpp" - -using namespace winrt::Windows; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - Foundation::IInspectable PercentageConverter::Convert(Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& /* targetType */, - Foundation::IInspectable const& /* parameter */, - hstring const& /* language */) - { - const auto decimal{ winrt::unbox_value(value) }; - const unsigned int number{ base::ClampMul(decimal, 100u) }; - return winrt::box_value(number); - } - - Foundation::IInspectable PercentageConverter::ConvertBack(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) }; - const auto decimal{ base::ClampDiv(number, 100) }; - return winrt::box_value(decimal); - } -} diff --git a/src/cascadia/TerminalSettingsEditor/PercentageConverter.h b/src/cascadia/TerminalSettingsEditor/PercentageConverter.h deleted file mode 100644 index e3aa16bbf..000000000 --- a/src/cascadia/TerminalSettingsEditor/PercentageConverter.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "PercentageConverter.g.h" -#include "Utils.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct PercentageConverter : PercentageConverterT - { - PercentageConverter() = default; - - Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - - Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value, - Windows::UI::Xaml::Interop::TypeName const& targetType, - Windows::Foundation::IInspectable const& parameter, - hstring const& language); - }; -} - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - BASIC_FACTORY(PercentageConverter); -} diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp index c7d9cd345..949c22e3b 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp @@ -24,6 +24,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _TerminalOutputHandlers(PreviewText); } + void PreviewConnection::Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) noexcept + { + } + void PreviewConnection::WriteInput(hstring const& /*data*/) { } diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h index 0c04664f0..7d4c773d7 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h @@ -22,6 +22,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation public: PreviewConnection() noexcept; + void Initialize(const Windows::Foundation::Collections::ValueSet& settings) noexcept; void Start() noexcept; void WriteInput(hstring const& data); void Resize(uint32_t rows, uint32_t columns) noexcept; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.cpp b/src/cascadia/TerminalSettingsEditor/Profiles.cpp index d49fc2476..94b49c2fa 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles.cpp @@ -18,11 +18,6 @@ using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Microsoft::Terminal::Settings::Model; -static const std::array InBoxProfileGuids{ - winrt::guid{ 0x61c54bbd, 0xc2c6, 0x5271, { 0x96, 0xe7, 0x00, 0x9a, 0x87, 0xff, 0x44, 0xbf } }, // Windows Powershell - winrt::guid{ 0x0caa0dad, 0x35be, 0x5f56, { 0xa8, 0xff, 0xaf, 0xce, 0xee, 0xaa, 0x61, 0x01 } } // Command Prompt -}; - namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { Windows::Foundation::Collections::IObservableVector ProfileViewModel::_MonospaceFontList{ nullptr }; @@ -221,25 +216,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation bool ProfileViewModel::CanDeleteProfile() const { - const auto guid{ Guid() }; - if (IsBaseLayer()) - { - return false; - } - else if (std::find(std::begin(InBoxProfileGuids), std::end(InBoxProfileGuids), guid) != std::end(InBoxProfileGuids)) - { - // in-box profile - return false; - } - else if (!Source().empty()) - { - // dynamic profile - return false; - } - else - { - return true; - } + return !IsBaseLayer(); } Editor::AppearanceViewModel ProfileViewModel::DefaultAppearance() @@ -275,7 +252,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _unfocusedAppearanceViewModel.Schemes(schemes); _unfocusedAppearanceViewModel.WindowRoot(windowRoot); - _NotifyChanges(L"UnfocusedAppearance", L"HasUnfocusedAppearance"); + _NotifyChanges(L"UnfocusedAppearance", L"HasUnfocusedAppearance", L"ShowUnfocusedAppearance"); } void ProfileViewModel::DeleteUnfocusedAppearance() @@ -284,7 +261,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _unfocusedAppearanceViewModel = nullptr; - _NotifyChanges(L"HasUnfocusedAppearance"); + _NotifyChanges(L"UnfocusedAppearance", L"HasUnfocusedAppearance", L"ShowUnfocusedAppearance"); } Editor::AppearanceViewModel ProfileViewModel::UnfocusedAppearance() @@ -383,21 +360,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation ProfileViewModel::UpdateFontList(); } - // Set the text disclaimer for the text box - hstring disclaimer{}; - const auto guid{ _State.Profile().Guid() }; - if (std::find(std::begin(InBoxProfileGuids), std::end(InBoxProfileGuids), guid) != std::end(InBoxProfileGuids)) - { - // load disclaimer for in-box profiles - disclaimer = RS_(L"Profile_DeleteButtonDisclaimerInBox"); - } - else if (!_State.Profile().Source().empty()) - { - // load disclaimer for dynamic profiles - disclaimer = RS_(L"Profile_DeleteButtonDisclaimerDynamic"); - } - DeleteButtonDisclaimer().Text(disclaimer); - // Check the use parent directory box if the starting directory is empty if (_State.Profile().StartingDirectory().empty()) { diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.h b/src/cascadia/TerminalSettingsEditor/Profiles.h index c39786bfd..e1ceba5d0 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.h +++ b/src/cascadia/TerminalSettingsEditor/Profiles.h @@ -20,6 +20,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Model::TerminalSettings TermSettings() const; + void SetAcrylicOpacityPercentageValue(double value) + { + AcrylicOpacity(winrt::Microsoft::Terminal::Settings::Editor::Converters::PercentageValueToPercentage(value)); + }; + + void SetPadding(double value) + { + Padding(to_hstring(value)); + } + // starting directory bool UseParentProcessDirectory(); void UseParentProcessDirectory(const bool useParent); @@ -143,7 +153,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Windows::Foundation::Collections::IMapView _Schemes; }; - struct Profiles : ProfilesT + struct Profiles : public HasScrollViewer, ProfilesT { public: Profiles(); diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.idl b/src/cascadia/TerminalSettingsEditor/Profiles.idl index ff8fc6565..4ecc48899 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.idl +++ b/src/cascadia/TerminalSettingsEditor/Profiles.idl @@ -19,6 +19,9 @@ namespace Microsoft.Terminal.Settings.Editor Windows.Foundation.Collections.IObservableVector MonospaceFontList { get; }; Microsoft.Terminal.Settings.Model.TerminalSettings TermSettings { get; }; + void SetAcrylicOpacityPercentageValue(Double value); + void SetPadding(Double value); + Boolean CanDeleteProfile { get; }; Boolean IsBaseLayer; Boolean UseParentProcessDirectory; @@ -105,5 +108,7 @@ namespace Microsoft.Terminal.Settings.Editor IInspectable CurrentScrollState; Windows.Foundation.Collections.IObservableVector ScrollStateList { get; }; + + Windows.UI.Xaml.Controls.Slider AcrylicOpacitySlider { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.xaml b/src/cascadia/TerminalSettingsEditor/Profiles.xaml index e2fb38987..46e7e87ef 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles.xaml @@ -33,17 +33,6 @@ - - - - - - - - - - - @@ -67,7 +56,7 @@ SelectionChanged="Pivot_SelectionChanged"> - + @@ -78,7 +67,7 @@ --> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> @@ -90,7 +79,7 @@ ClearSettingValue="{x:Bind State.Profile.ClearCommandline}" HasSettingValue="{x:Bind State.Profile.HasCommandline, Mode=OneWay}" SettingOverrideSource="{x:Bind State.Profile.CommandlineOverrideSource, Mode=OneWay}" - Visibility="{x:Bind State.Profile.IsBaseLayer, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}"> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> @@ -150,93 +139,89 @@ + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> - - - - + + + - + + Value="{x:Bind local:Converters.PercentageToPercentageValue(State.Profile.AcrylicOpacity), BindBack=State.Profile.SetAcrylicOpacityPercentageValue, Mode=TwoWay}" /> + Text="{x:Bind local:Converters.AppendPercentageSign(AcrylicOpacitySlider.Value), Mode=OneWay}" /> @@ -307,7 +292,7 @@ + Value="{x:Bind local:Converters.MaxValueFromPaddingString(State.Profile.Padding), BindBack=State.Profile.SetPadding, Mode=TwoWay}" /> @@ -333,7 +318,7 @@ Margin="32,0,0,0" Click="CreateUnfocusedAppearance_Click" Style="{StaticResource BaseButtonStyle}" - Visibility="{x:Bind State.Profile.HasUnfocusedAppearance, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}"> + Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.HasUnfocusedAppearance), Mode=OneWay}"> @@ -408,7 +393,7 @@ - + + struct ReadOnlyActions : public HasScrollViewer, ReadOnlyActionsT { public: ReadOnlyActions(); diff --git a/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml b/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml index ec1a685fb..c5c1844cc 100644 --- a/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml +++ b/src/cascadia/TerminalSettingsEditor/ReadOnlyActions.xaml @@ -17,9 +17,6 @@ - - - ID) + // KeyChord --> ID actionMap->_KeyMap = _KeyMap; - // copy _ActionMap (ID --> Command) + // ID --> Command + actionMap->_ActionMap.reserve(_ActionMap.size()); for (const auto& [actionID, cmd] : _ActionMap) { - actionMap->_ActionMap.emplace(actionID, *(get_self(cmd)->Copy())); + actionMap->_ActionMap.emplace(actionID, *winrt::get_self(cmd)->Copy()); } - // copy _MaskingActions (ID --> Command) + // ID --> Command + actionMap->_MaskingActions.reserve(_MaskingActions.size()); for (const auto& [actionID, cmd] : _MaskingActions) { - actionMap->_MaskingActions.emplace(actionID, *(get_self(cmd)->Copy())); + actionMap->_MaskingActions.emplace(actionID, *winrt::get_self(cmd)->Copy()); } - // copy _NestedCommands (Name --> Command) + // Name --> Command + actionMap->_NestedCommands.reserve(_NestedCommands.size()); for (const auto& [name, cmd] : _NestedCommands) { - actionMap->_NestedCommands.Insert(name, *(get_self(cmd)->Copy())); + actionMap->_NestedCommands.emplace(name, *winrt::get_self(cmd)->Copy()); } - // copy _IterableCommands + actionMap->_IterableCommands.reserve(_IterableCommands.size()); for (const auto& cmd : _IterableCommands) { - actionMap->_IterableCommands.Append(*(get_self(cmd)->Copy())); + actionMap->_IterableCommands.emplace_back(*winrt::get_self(cmd)->Copy()); } - // repeat this for each of our parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); + actionMap->_parents.reserve(_parents.size()); for (const auto& parent : _parents) { actionMap->_parents.emplace_back(parent->Copy()); @@ -431,7 +435,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto name{ cmd.Name() }; if (!name.empty()) { - _NestedCommands.Insert(name, cmd); + _NestedCommands.emplace(name, cmd); } return; } @@ -439,7 +443,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Handle iterable commands if (cmdImpl->IterateOn() != ExpandCommandType::None) { - _IterableCommands.Append(cmd); + _IterableCommands.emplace_back(cmd); return; } @@ -513,9 +517,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // If we had to find one from a layer above that, parent->_MaskingActions // would have found it, so we inherit it for free! const auto& inheritedCmd{ parent->_GetActionByID(actionID) }; - if (inheritedCmd.has_value() && inheritedCmd.value()) + if (inheritedCmd && *inheritedCmd) { - const auto& inheritedCmdImpl{ get_self(inheritedCmd.value()) }; + const auto& inheritedCmdImpl{ get_self(*inheritedCmd) }; maskingCmd = *inheritedCmdImpl->Copy(); _MaskingActions.emplace(actionID, maskingCmd); } @@ -582,7 +586,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } // Handle a collision with NestedCommands - _NestedCommands.TryRemove(newName); + _NestedCommands.erase(newName); } // Method Description: @@ -677,24 +681,31 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } } + // Method Description: + // - Determines whether the given key chord is explicitly unbound + // Arguments: + // - keys: the key chord to check + // Return value: + // - true if the keychord is explicitly unbound + // - false if either the keychord is bound, or not bound at all + bool ActionMap::IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const + { + // We use the fact that the ..Internal call returns nullptr for explicitly unbound + // key chords, and nullopt for keychord that are not bound - it allows us to distinguish + // between unbound and lack of binding. + return _GetActionByKeyChordInternal(keys) == nullptr; + } + // Method Description: // - Retrieves the assigned command that can be invoked with the given key chord // Arguments: // - keys: the key chord of the command to search for // Return Value: // - the command with the given key chord - // - nullptr if the key chord is explicitly unbound + // - nullptr if the key chord doesn't exist Model::Command ActionMap::GetActionByKeyChord(Control::KeyChord const& keys) const { - // Check the current layer - const auto cmd{ _GetActionByKeyChordInternal(keys) }; - if (cmd.has_value()) - { - return *cmd; - } - - // This key chord is not explicitly bound - return nullptr; + return _GetActionByKeyChordInternal(keys).value_or(nullptr); } // Method Description: @@ -706,11 +717,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // - the command with the given key chord // - nullptr if the key chord is explicitly unbound // - nullopt if it was not bound in this layer - std::optional ActionMap::_GetActionByKeyChordInternal(Control::KeyChord const& keys) const + std::optional ActionMap::_GetActionByKeyChordInternal(const Control::KeyChord& keys) const { // Check the current layer - const auto actionIDPair{ _KeyMap.find(keys) }; - if (actionIDPair != _KeyMap.end()) + if (const auto actionIDPair = _KeyMap.find(keys); actionIDPair != _KeyMap.end()) { // the command was explicitly bound, // return what we found (invalid commands exposed as nullptr) @@ -719,11 +729,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // the command was not bound in this layer, // ask my parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); for (const auto& parent : _parents) { const auto& inheritedCmd{ parent->_GetActionByKeyChordInternal(keys) }; - if (inheritedCmd.has_value()) + if (inheritedCmd) { return *inheritedCmd; } @@ -765,11 +775,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto hash{ Hash(actionAndArgs) }; if (const auto& cmd{ _GetActionByID(hash) }) { - return cmd.value().Keys(); + return cmd->Keys(); } // Check our parents - FAIL_FAST_IF(_parents.size() > 1); + assert(_parents.size() <= 1); for (const auto& parent : _parents) { if (const auto& keys{ parent->GetKeyBindingForAction(myAction, myArgs) }) diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 32983767e..8cc314241 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -34,24 +34,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct KeyChordHash { - std::size_t operator()(const Control::KeyChord& key) const + inline std::size_t operator()(const Control::KeyChord& key) const { - return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(key.Modifiers(), key.Vkey()); + return static_cast(key.Hash()); } }; struct KeyChordEquality { - bool operator()(const Control::KeyChord& lhs, const Control::KeyChord& rhs) const + inline bool operator()(const Control::KeyChord& lhs, const Control::KeyChord& rhs) const { - return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey(); + return lhs.Equals(rhs); } }; struct ActionMap : ActionMapT, IInheritable { - ActionMap(); - // views Windows::Foundation::Collections::IMapView AvailableActions(); Windows::Foundation::Collections::IMapView NameMap(); @@ -61,6 +59,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // queries Model::Command GetActionByKeyChord(Control::KeyChord const& keys) const; + bool IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const; Control::KeyChord GetKeyBindingForAction(ShortcutAction const& action) const; Control::KeyChord GetKeyBindingForAction(ShortcutAction const& action, IActionArgs const& actionArgs) const; @@ -79,8 +78,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation private: std::optional _GetActionByID(const InternalActionID actionID) const; - std::optional _GetActionByKeyChordInternal(Control::KeyChord const& keys) const; + std::optional _GetActionByKeyChordInternal(const Control::KeyChord& keys) const; + void _RefreshKeyBindingCaches(); void _PopulateAvailableActionsWithStandardCommands(std::unordered_map& availableActions, std::unordered_set& visitedActionIDs) const; void _PopulateNameMapWithSpecialCommands(std::unordered_map& nameMap) const; void _PopulateNameMapWithStandardCommands(std::unordered_map& nameMap) const; @@ -95,8 +95,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; 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 _NestedCommands; + std::vector _IterableCommands; std::unordered_map _KeyMap; std::unordered_map _ActionMap; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.idl b/src/cascadia/TerminalSettingsModel/ActionMap.idl index 0a9c4d07c..806baa17a 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -8,6 +8,8 @@ namespace Microsoft.Terminal.Settings.Model // This interface ensures that no changes are made to ActionMap interface IActionMapView { + Boolean IsKeyChordExplicitlyUnbound(Microsoft.Terminal.Control.KeyChord keys); + Command GetActionByKeyChord(Microsoft.Terminal.Control.KeyChord keys); Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action); diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index cc3559037..7d0d0a728 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -23,58 +23,61 @@ // each action. This is _NOT_ something that should be used when any individual // case should be customized. -#define ALL_SHORTCUT_ACTIONS \ - ON_ALL_ACTIONS(CopyText) \ - ON_ALL_ACTIONS(PasteText) \ - ON_ALL_ACTIONS(OpenNewTabDropdown) \ - ON_ALL_ACTIONS(DuplicateTab) \ - ON_ALL_ACTIONS(NewTab) \ - ON_ALL_ACTIONS(CloseWindow) \ - ON_ALL_ACTIONS(CloseTab) \ - ON_ALL_ACTIONS(ClosePane) \ - ON_ALL_ACTIONS(NextTab) \ - ON_ALL_ACTIONS(PrevTab) \ - ON_ALL_ACTIONS(SendInput) \ - ON_ALL_ACTIONS(SplitPane) \ - ON_ALL_ACTIONS(TogglePaneZoom) \ - ON_ALL_ACTIONS(SwitchToTab) \ - ON_ALL_ACTIONS(AdjustFontSize) \ - ON_ALL_ACTIONS(ResetFontSize) \ - ON_ALL_ACTIONS(ScrollUp) \ - ON_ALL_ACTIONS(ScrollDown) \ - ON_ALL_ACTIONS(ScrollUpPage) \ - ON_ALL_ACTIONS(ScrollDownPage) \ - ON_ALL_ACTIONS(ScrollToTop) \ - ON_ALL_ACTIONS(ScrollToBottom) \ - ON_ALL_ACTIONS(ResizePane) \ - ON_ALL_ACTIONS(MoveFocus) \ - ON_ALL_ACTIONS(Find) \ - ON_ALL_ACTIONS(ToggleShaderEffects) \ - ON_ALL_ACTIONS(ToggleFocusMode) \ - ON_ALL_ACTIONS(ToggleFullscreen) \ - ON_ALL_ACTIONS(ToggleAlwaysOnTop) \ - ON_ALL_ACTIONS(OpenSettings) \ - ON_ALL_ACTIONS(SetColorScheme) \ - ON_ALL_ACTIONS(SetTabColor) \ - ON_ALL_ACTIONS(OpenTabColorPicker) \ - ON_ALL_ACTIONS(RenameTab) \ - ON_ALL_ACTIONS(OpenTabRenamer) \ - ON_ALL_ACTIONS(ExecuteCommandline) \ - ON_ALL_ACTIONS(ToggleCommandPalette) \ - ON_ALL_ACTIONS(CloseOtherTabs) \ - ON_ALL_ACTIONS(CloseTabsAfter) \ - ON_ALL_ACTIONS(TabSearch) \ - ON_ALL_ACTIONS(MoveTab) \ - ON_ALL_ACTIONS(BreakIntoDebugger) \ - ON_ALL_ACTIONS(TogglePaneReadOnly) \ - ON_ALL_ACTIONS(FindMatch) \ - ON_ALL_ACTIONS(NewWindow) \ - ON_ALL_ACTIONS(IdentifyWindow) \ - ON_ALL_ACTIONS(IdentifyWindows) \ - ON_ALL_ACTIONS(RenameWindow) \ - ON_ALL_ACTIONS(OpenWindowRenamer) \ - ON_ALL_ACTIONS(GlobalSummon) \ - ON_ALL_ACTIONS(QuakeMode) \ +#define ALL_SHORTCUT_ACTIONS \ + ON_ALL_ACTIONS(CopyText) \ + ON_ALL_ACTIONS(PasteText) \ + ON_ALL_ACTIONS(OpenNewTabDropdown) \ + ON_ALL_ACTIONS(DuplicateTab) \ + ON_ALL_ACTIONS(NewTab) \ + ON_ALL_ACTIONS(CloseWindow) \ + ON_ALL_ACTIONS(CloseTab) \ + ON_ALL_ACTIONS(ClosePane) \ + ON_ALL_ACTIONS(NextTab) \ + ON_ALL_ACTIONS(PrevTab) \ + ON_ALL_ACTIONS(SendInput) \ + ON_ALL_ACTIONS(SplitPane) \ + ON_ALL_ACTIONS(ToggleSplitOrientation) \ + ON_ALL_ACTIONS(TogglePaneZoom) \ + ON_ALL_ACTIONS(SwitchToTab) \ + ON_ALL_ACTIONS(AdjustFontSize) \ + ON_ALL_ACTIONS(ResetFontSize) \ + ON_ALL_ACTIONS(ScrollUp) \ + ON_ALL_ACTIONS(ScrollDown) \ + ON_ALL_ACTIONS(ScrollUpPage) \ + ON_ALL_ACTIONS(ScrollDownPage) \ + ON_ALL_ACTIONS(ScrollToTop) \ + ON_ALL_ACTIONS(ScrollToBottom) \ + ON_ALL_ACTIONS(ResizePane) \ + ON_ALL_ACTIONS(MoveFocus) \ + ON_ALL_ACTIONS(MovePane) \ + ON_ALL_ACTIONS(SwapPane) \ + ON_ALL_ACTIONS(Find) \ + ON_ALL_ACTIONS(ToggleShaderEffects) \ + ON_ALL_ACTIONS(ToggleFocusMode) \ + ON_ALL_ACTIONS(ToggleFullscreen) \ + ON_ALL_ACTIONS(ToggleAlwaysOnTop) \ + ON_ALL_ACTIONS(OpenSettings) \ + ON_ALL_ACTIONS(SetColorScheme) \ + ON_ALL_ACTIONS(SetTabColor) \ + ON_ALL_ACTIONS(OpenTabColorPicker) \ + ON_ALL_ACTIONS(RenameTab) \ + ON_ALL_ACTIONS(OpenTabRenamer) \ + ON_ALL_ACTIONS(ExecuteCommandline) \ + ON_ALL_ACTIONS(ToggleCommandPalette) \ + ON_ALL_ACTIONS(CloseOtherTabs) \ + ON_ALL_ACTIONS(CloseTabsAfter) \ + ON_ALL_ACTIONS(TabSearch) \ + ON_ALL_ACTIONS(MoveTab) \ + ON_ALL_ACTIONS(BreakIntoDebugger) \ + ON_ALL_ACTIONS(TogglePaneReadOnly) \ + ON_ALL_ACTIONS(FindMatch) \ + ON_ALL_ACTIONS(NewWindow) \ + ON_ALL_ACTIONS(IdentifyWindow) \ + ON_ALL_ACTIONS(IdentifyWindows) \ + ON_ALL_ACTIONS(RenameWindow) \ + ON_ALL_ACTIONS(OpenWindowRenamer) \ + ON_ALL_ACTIONS(GlobalSummon) \ + ON_ALL_ACTIONS(QuakeMode) \ ON_ALL_ACTIONS(FocusPane) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ @@ -87,6 +90,8 @@ ON_ALL_ACTIONS_WITH_ARGS(FindMatch) \ ON_ALL_ACTIONS_WITH_ARGS(GlobalSummon) \ ON_ALL_ACTIONS_WITH_ARGS(MoveFocus) \ + ON_ALL_ACTIONS_WITH_ARGS(MovePane) \ + ON_ALL_ACTIONS_WITH_ARGS(SwapPane) \ ON_ALL_ACTIONS_WITH_ARGS(MoveTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewTab) \ ON_ALL_ACTIONS_WITH_ARGS(NewWindow) \ diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp b/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp index a7343a51b..67eb279ea 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp @@ -25,6 +25,7 @@ static constexpr std::string_view BackgroundImageStretchModeKey{ "backgroundImag static constexpr std::string_view BackgroundImageAlignmentKey{ "backgroundImageAlignment" }; static constexpr std::string_view RetroTerminalEffectKey{ "experimental.retroTerminalEffect" }; static constexpr std::string_view PixelShaderPathKey{ "experimental.pixelShaderPath" }; +static constexpr std::string_view IntenseTextStyleKey{ "intenseTextStyle" }; winrt::Microsoft::Terminal::Settings::Model::implementation::AppearanceConfig::AppearanceConfig(const winrt::weak_ref sourceProfile) : _sourceProfile(sourceProfile) @@ -48,6 +49,7 @@ winrt::com_ptr AppearanceConfig::CopyAppearance(const winrt::c appearance->_BackgroundImageAlignment = sourceAppearance->_BackgroundImageAlignment; appearance->_RetroTerminalEffect = sourceAppearance->_RetroTerminalEffect; appearance->_PixelShaderPath = sourceAppearance->_PixelShaderPath; + appearance->_IntenseTextStyle = sourceAppearance->_IntenseTextStyle; return appearance; } @@ -68,6 +70,7 @@ Json::Value AppearanceConfig::ToJson() const JsonUtils::SetValueForKey(json, BackgroundImageAlignmentKey, _BackgroundImageAlignment); JsonUtils::SetValueForKey(json, RetroTerminalEffectKey, _RetroTerminalEffect); JsonUtils::SetValueForKey(json, PixelShaderPathKey, _PixelShaderPath); + JsonUtils::SetValueForKey(json, IntenseTextStyleKey, _IntenseTextStyle); return json; } @@ -98,6 +101,7 @@ void AppearanceConfig::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, BackgroundImageAlignmentKey, _BackgroundImageAlignment); JsonUtils::GetValueForKey(json, RetroTerminalEffectKey, _RetroTerminalEffect); JsonUtils::GetValueForKey(json, PixelShaderPathKey, _PixelShaderPath); + JsonUtils::GetValueForKey(json, IntenseTextStyleKey, _IntenseTextStyle); } winrt::Microsoft::Terminal::Settings::Model::Profile AppearanceConfig::SourceProfile() diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h index cdcdc9613..7a0c47985 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h @@ -52,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::IAppearanceConfig, bool, RetroTerminalEffect, false); INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, PixelShaderPath, L""); + INHERITABLE_SETTING(Model::IAppearanceConfig, Model::IntenseStyle, IntenseTextStyle, Model::IntenseStyle::Bright); private: winrt::weak_ref _sourceProfile; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index ee685d659..052026fb9 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -136,13 +136,12 @@ void CascadiaSettings::_CopyProfileInheritanceTree(winrt::com_ptrCreateChild() }; - newProfile->Name(newName); + const auto newProfile = _CreateNewProfile(newName); _allProfiles.Append(*newProfile); + _activeProfiles.Append(*newProfile); return *newProfile; } @@ -259,26 +258,10 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate { THROW_HR_IF_NULL(E_INVALIDARG, source); - winrt::com_ptr duplicated; - if (_userDefaultProfileSettings) - { - duplicated = _userDefaultProfileSettings->CreateChild(); - } - else - { - duplicated = winrt::make_self(); - } - _allProfiles.Append(*duplicated); - - if (!source.Hidden()) - { - _activeProfiles.Append(*duplicated); - } - - winrt::hstring newName{ fmt::format(L"{} ({})", source.Name(), RS_(L"CopySuffix")) }; + auto newName = fmt::format(L"{} ({})", source.Name(), RS_(L"CopySuffix")); // Check if this name already exists and if so, append a number - for (uint32_t candidateIndex = 0; candidateIndex < _allProfiles.Size() + 1; ++candidateIndex) + for (uint32_t candidateIndex = 0, count = _allProfiles.Size() + 1; candidateIndex < count; ++candidateIndex) { if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; })) { @@ -287,13 +270,14 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate // There is a theoretical unsigned integer wraparound, which is OK newName = fmt::format(L"{} ({} {})", source.Name(), RS_(L"CopySuffix"), candidateIndex + 2); } - duplicated->Name(winrt::hstring(newName)); - const auto isProfilesDefaultsOrigin = [](const auto& profile) -> bool { + const auto duplicated = _CreateNewProfile(newName); + + static constexpr auto isProfilesDefaultsOrigin = [](const auto& profile) -> bool { return profile && profile.Origin() != OriginTag::ProfilesDefaults; }; - const auto isProfilesDefaultsOriginSub = [=](const auto& sub) -> bool { + static constexpr auto isProfilesDefaultsOriginSub = [](const auto& sub) -> bool { return sub && isProfilesDefaultsOrigin(sub.SourceProfile()); }; @@ -309,7 +293,9 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate target.settingName(source.settingName()); \ } - DUPLICATE_SETTING_MACRO(Hidden); + // If the source is hidden and the Settings UI creates a + // copy of it we don't want the copy to be hidden as well. + // --> Don't do DUPLICATE_SETTING_MACRO(Hidden); DUPLICATE_SETTING_MACRO(Icon); DUPLICATE_SETTING_MACRO(CloseOnExit); DUPLICATE_SETTING_MACRO(TabTitle); @@ -335,6 +321,8 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate DUPLICATE_SETTING_MACRO_SUB(font, target, FontFace); DUPLICATE_SETTING_MACRO_SUB(font, target, FontSize); DUPLICATE_SETTING_MACRO_SUB(font, target, FontWeight); + DUPLICATE_SETTING_MACRO_SUB(font, target, FontFeatures); + DUPLICATE_SETTING_MACRO_SUB(font, target, FontAxes); } { @@ -346,6 +334,7 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate DUPLICATE_SETTING_MACRO_SUB(appearance, target, SelectionBackground); DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorColor); DUPLICATE_SETTING_MACRO_SUB(appearance, target, PixelShaderPath); + DUPLICATE_SETTING_MACRO_SUB(appearance, target, IntenseTextStyle); DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImagePath); DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageOpacity); DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageStretchMode); @@ -386,6 +375,8 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate duplicated->ConnectionType(source.ConnectionType()); } + _allProfiles.Append(*duplicated); + _activeProfiles.Append(*duplicated); return *duplicated; } @@ -419,6 +410,32 @@ winrt::hstring CascadiaSettings::GetSerializationErrorMessage() return _deserializationErrorMessage; } +// As used by CreateNewProfile and DuplicateProfile this function +// creates a new Profile instance with a random UUID and a given name. +winrt::com_ptr CascadiaSettings::_CreateNewProfile(const std::wstring_view& name) const +{ + winrt::com_ptr profile; + + if (_userDefaultProfileSettings) + { + profile = _userDefaultProfileSettings->CreateChild(); + } + else + { + profile = winrt::make_self(); + } + + // Technically there's Utils::CreateV5Uuid which we could use, but I wanted + // truly globally unique UUIDs for profiles created through the settings UI. + GUID guid{}; + LOG_IF_FAILED(CoCreateGuid(&guid)); + + profile->Guid(guid); + profile->Name(winrt::hstring{ name }); + + return profile; +} + // Method Description: // - Attempts to validate this settings structure. If there are critical errors // found, they'll be thrown as a SettingsLoadError. Non-critical errors, such @@ -802,7 +819,7 @@ void CascadiaSettings::_ValidateMediaResources() // and attempt to look the profile up by name instead. // Return Value: // - the GUID of the profile corresponding to this combination of index and NewTerminalArgs -winrt::guid CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs& newTerminalArgs) const +winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs& newTerminalArgs) const { std::optional profileByIndex, profileByName; if (newTerminalArgs) @@ -815,7 +832,7 @@ winrt::guid CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs& ne profileByName = _GetProfileGuidByName(newTerminalArgs.Profile()); } - return til::coalesce_value(profileByName, profileByIndex, _globals->DefaultProfile()); + return FindProfile(til::coalesce_value(profileByName, profileByIndex, _globals->DefaultProfile())); } // Method Description: @@ -1040,9 +1057,8 @@ std::string CascadiaSettings::_ApplyFirstRunChangesToSettingsTemplate(std::strin // - profileGuid: the GUID of the profile to find the scheme for. // Return Value: // - a non-owning pointer to the scheme. -winrt::Microsoft::Terminal::Settings::Model::ColorScheme CascadiaSettings::GetColorSchemeForProfile(const winrt::guid profileGuid) const +winrt::Microsoft::Terminal::Settings::Model::ColorScheme CascadiaSettings::GetColorSchemeForProfile(const Model::Profile& profile) const { - auto profile = FindProfile(profileGuid); if (!profile) { return nullptr; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 5bb37b309..ee5782277 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -86,8 +86,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::hstring ApplicationVersion(); Model::Profile CreateNewProfile(); - Model::Profile FindProfile(guid profileGuid) const noexcept; - Model::ColorScheme GetColorSchemeForProfile(const guid profileGuid) const; + Model::Profile FindProfile(const guid& profileGuid) const noexcept; + Model::ColorScheme GetColorSchemeForProfile(const Model::Profile& profile) const; void UpdateColorSchemeReferences(const hstring oldName, const hstring newName); Windows::Foundation::Collections::IVectorView Warnings(); @@ -96,7 +96,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::IReference GetLoadingError(); hstring GetSerializationErrorMessage(); - winrt::guid GetProfileForArgs(const Model::NewTerminalArgs& newTerminalArgs) const; + Model::Profile GetProfileForArgs(const Model::NewTerminalArgs& newTerminalArgs) const; Model::Profile DuplicateProfile(const Model::Profile& source); void RefreshDefaultTerminals(); @@ -124,6 +124,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value _defaultSettings; winrt::com_ptr _userDefaultProfileSettings{ nullptr }; + winrt::com_ptr _CreateNewProfile(const std::wstring_view& name) const; + void _LayerOrCreateProfile(const Json::Value& profileJson); winrt::com_ptr _FindMatchingProfile(const Json::Value& profileJson); std::optional _FindMatchingProfileIndex(const Json::Value& profileJson); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl index 85882ec08..2f3248fbc 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl @@ -41,10 +41,10 @@ namespace Microsoft.Terminal.Settings.Model Profile CreateNewProfile(); Profile FindProfile(Guid profileGuid); - ColorScheme GetColorSchemeForProfile(Guid profileGuid); + ColorScheme GetColorSchemeForProfile(Profile profile); void UpdateColorSchemeReferences(String oldName, String newName); - Guid GetProfileForArgs(NewTerminalArgs newTerminalArgs); + Profile GetProfileForArgs(NewTerminalArgs newTerminalArgs); void RefreshDefaultTerminals(); static Boolean IsDefaultTerminalAvailable { get; }; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index bcefe5b77..36ca2a316 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -155,15 +155,14 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: const auto hardcodedDefaultGuid = resultPtr->GlobalSettings().DefaultProfile(); std::optional fileData = _ReadUserSettings(); - const bool foundFile = fileData.has_value(); // Make sure the file isn't totally empty. If it is, we'll treat the file // like it doesn't exist at all. - const bool fileHasData = foundFile && !fileData.value().empty(); + const bool fileHasData = fileData && !fileData->empty(); bool needToWriteFile = false; if (fileHasData) { - resultPtr->_ParseJsonString(fileData.value(), false); + resultPtr->_ParseJsonString(*fileData, false); } // Load profiles from dynamic profile generators. _userSettings should be @@ -204,6 +203,38 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: _CatchRethrowSerializationExceptionWithLocationInfo(resultPtr->_userSettingsString); } + // Let's say a user doesn't know that they need to write `"hidden": true` in + // order to prevent a profile from showing up (and a settings UI doesn't exist). + // Naturally they would open settings.json and try to remove the profile object. + // This section of code recognizes if a profile was seen before and marks it as + // `"hidden": true` by default and thus ensures the behavior the user expects: + // Profiles won't show up again after they've been removed from settings.json. + { + const auto state = winrt::get_self(ApplicationState::SharedInstance()); + auto generatedProfiles = state->GeneratedProfiles(); + bool generatedProfilesChanged = false; + + for (const auto& profile : resultPtr->_allProfiles) + { + const auto profileImpl = winrt::get_self(profile); + + if (generatedProfiles.emplace(profileImpl->Guid()).second) + { + generatedProfilesChanged = true; + } + else if (profileImpl->Origin() != OriginTag::User) + { + profileImpl->Deleted(true); + profileImpl->Hidden(true); + } + } + + if (generatedProfilesChanged) + { + state->GeneratedProfiles(generatedProfiles); + } + } + // After layering the user settings, check if there are any new profiles // that need to be inserted into their user settings file. needToWriteFile = resultPtr->_AppendDynamicProfilesToUserSettings() || needToWriteFile; @@ -323,7 +354,7 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: // tag these profiles as in-box for (const auto& profile : resultPtr->AllProfiles()) { - const auto& profileImpl{ winrt::get_self(profile) }; + const auto profileImpl{ winrt::get_self(profile) }; profileImpl->Origin(OriginTag::InBox); } @@ -352,7 +383,6 @@ void CascadiaSettings::_LoadDynamicProfiles() } } - const GUID nullGuid{ 0 }; for (auto& generator : _profileGenerators) { const std::wstring generatorNamespace{ generator->GetNamespace() }; @@ -711,7 +741,7 @@ bool CascadiaSettings::_AppendDynamicProfilesToUserSettings() wbuilder.settings_["indentation"] = " "; wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons - auto isInJsonObj = [](const auto& profile, const auto& json) { + static const auto isInJsonObj = [](const auto& profile, const auto& json) { for (auto profileJson : _GetProfilesJsonObject(json)) { if (profileJson.isObject()) @@ -745,8 +775,16 @@ bool CascadiaSettings::_AppendDynamicProfilesToUserSettings() for (const auto& profile : _allProfiles) { - // Skip profiles that are in the user settings or the default settings. - if (isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings)) + // Skip profiles that are: + // * hidden + // Because when a user manually removes profiles from settings.json, + // we mark them as hidden in LoadAll(). Adding those profiles right + // back into settings.json would feel confusing, while the + // profile that was just erased is added right back. + // * in the user settings or the default settings + // Because we don't want to add profiles which are already + // in the settings.json (explicitly or implicitly). + if (profile.Deleted() || isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings)) { continue; } @@ -1215,8 +1253,11 @@ Json::Value CascadiaSettings::ToJson() const Json::Value profilesList{ Json::ValueType::arrayValue }; for (const auto& entry : _allProfiles) { - const auto prof{ winrt::get_self(entry) }; - profilesList.append(prof->ToJson()); + if (!entry.Deleted()) + { + const auto prof{ winrt::get_self(entry) }; + profilesList.append(prof->ToJson()); + } } profiles[JsonKey(ProfilesListKey)] = profilesList; json[JsonKey(ProfilesKey)] = profiles; diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp index b8a2e5f52..e0c9d2b45 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp @@ -43,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation DEFINE_ENUM_MAP(Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode); DEFINE_ENUM_MAP(Microsoft::Terminal::Control::TextAntialiasingMode, TextAntialiasingMode); DEFINE_ENUM_MAP(Microsoft::Terminal::Core::CursorStyle, CursorStyle); + DEFINE_ENUM_MAP(Microsoft::Terminal::Settings::Model::IntenseStyle, IntenseTextStyle); // FontWeight is special because the JsonUtils::ConversionTrait for it // creates a FontWeight object, but we need to use the uint16_t value. diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.h b/src/cascadia/TerminalSettingsModel/EnumMappings.h index b9ca05ae1..c597c6a77 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.h +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.h @@ -40,6 +40,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::Windows::Foundation::Collections::IMap TextAntialiasingMode(); static winrt::Windows::Foundation::Collections::IMap CursorStyle(); static winrt::Windows::Foundation::Collections::IMap FontWeight(); + static winrt::Windows::Foundation::Collections::IMap IntenseTextStyle(); }; } diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.idl b/src/cascadia/TerminalSettingsModel/EnumMappings.idl index 7b9e6609e..4a7bb593b 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.idl +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.idl @@ -22,5 +22,6 @@ namespace Microsoft.Terminal.Settings.Model static Windows.Foundation.Collections.IMap TextAntialiasingMode { get; }; static Windows.Foundation.Collections.IMap CursorStyle { get; }; static Windows.Foundation.Collections.IMap FontWeight { get; }; + static Windows.Foundation.Collections.IMap IntenseTextStyle { get; }; } } diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp index b952f6a3d..81fbb6cee 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp @@ -123,7 +123,27 @@ namespace Microsoft::Terminal::Settings::Model void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content) { - auto tmpPath = path; + // GH#10787: rename() will replace symbolic links themselves and not the path they point at. + // It's thus important that we first resolve them before generating temporary path. + std::error_code ec; + const auto resolvedPath = std::filesystem::is_symlink(path) ? std::filesystem::canonical(path, ec) : path; + if (ec) + { + if (ec.value() != ERROR_FILE_NOT_FOUND) + { + THROW_WIN32_MSG(ec.value(), "failed to compute canonical path"); + } + + // The original file is a symbolic link, but the target doesn't exist. + // Consider two fall-backs: + // * resolve the link manually, which might be less accurate and more prone to race conditions + // * write to the file directly, which lets the system resolve the symbolic link but leaves the write non-atomic + // The latter is chosen, as this is an edge case and our 'atomic' writes are only best-effort. + WriteUTF8File(path, content); + return; + } + + auto tmpPath = resolvedPath; tmpPath += L".tmp"; // Writing to a file isn't atomic, but... @@ -132,6 +152,6 @@ namespace Microsoft::Terminal::Settings::Model // renaming one is (supposed to be) atomic. // Wait... "supposed to be"!? Well it's technically not always atomic, // but it's pretty darn close to it, so... better than nothing. - std::filesystem::rename(tmpPath, path); + std::filesystem::rename(tmpPath, resolvedPath); } } diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.cpp b/src/cascadia/TerminalSettingsModel/FontConfig.cpp index a7c22a581..2d5f1bcd9 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/FontConfig.cpp @@ -14,6 +14,8 @@ static constexpr std::string_view FontInfoKey{ "font" }; static constexpr std::string_view FontFaceKey{ "face" }; static constexpr std::string_view FontSizeKey{ "size" }; static constexpr std::string_view FontWeightKey{ "weight" }; +static constexpr std::string_view FontFeaturesKey{ "features" }; +static constexpr std::string_view FontAxesKey{ "axes" }; static constexpr std::string_view LegacyFontFaceKey{ "fontFace" }; static constexpr std::string_view LegacyFontSizeKey{ "fontSize" }; static constexpr std::string_view LegacyFontWeightKey{ "fontWeight" }; @@ -29,6 +31,8 @@ winrt::com_ptr FontConfig::CopyFontInfo(const winrt::com_ptr_FontFace = source->_FontFace; fontInfo->_FontSize = source->_FontSize; fontInfo->_FontWeight = source->_FontWeight; + fontInfo->_FontAxes = source->_FontAxes; + fontInfo->_FontFeatures = source->_FontFeatures; return fontInfo; } @@ -39,6 +43,8 @@ Json::Value FontConfig::ToJson() const JsonUtils::SetValueForKey(json, FontFaceKey, _FontFace); JsonUtils::SetValueForKey(json, FontSizeKey, _FontSize); JsonUtils::SetValueForKey(json, FontWeightKey, _FontWeight); + JsonUtils::SetValueForKey(json, FontAxesKey, _FontAxes); + JsonUtils::SetValueForKey(json, FontFeaturesKey, _FontFeatures); return json; } @@ -65,6 +71,8 @@ void FontConfig::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(fontInfoJson, FontFaceKey, _FontFace); JsonUtils::GetValueForKey(fontInfoJson, FontSizeKey, _FontSize); JsonUtils::GetValueForKey(fontInfoJson, FontWeightKey, _FontWeight); + JsonUtils::GetValueForKey(fontInfoJson, FontFeaturesKey, _FontFeatures); + JsonUtils::GetValueForKey(fontInfoJson, FontAxesKey, _FontAxes); } else { diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.h b/src/cascadia/TerminalSettingsModel/FontConfig.h index 5ed2bef3d..61f816dba 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.h +++ b/src/cascadia/TerminalSettingsModel/FontConfig.h @@ -23,6 +23,9 @@ Author(s): #include "IInheritable.h" #include +using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { struct FontConfig : FontConfigT, IInheritable @@ -39,6 +42,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::FontConfig, hstring, FontFace, DEFAULT_FONT_FACE); INHERITABLE_SETTING(Model::FontConfig, int32_t, FontSize, DEFAULT_FONT_SIZE); INHERITABLE_SETTING(Model::FontConfig, Windows::UI::Text::FontWeight, FontWeight, DEFAULT_FONT_WEIGHT); + INHERITABLE_SETTING(Model::FontConfig, IFontAxesMap, FontAxes); + INHERITABLE_SETTING(Model::FontConfig, IFontFeatureMap, FontFeatures); private: winrt::weak_ref _sourceProfile; diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.idl b/src/cascadia/TerminalSettingsModel/FontConfig.idl index 7bca0ef11..37d2aeb80 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.idl +++ b/src/cascadia/TerminalSettingsModel/FontConfig.idl @@ -8,6 +8,8 @@ import "Profile.idl"; _BASE_INHERITABLE_SETTING(Type, Name); \ Microsoft.Terminal.Settings.Model.FontConfig Name##OverrideSource { get; } +#define COMMA , + namespace Microsoft.Terminal.Settings.Model { [default_interface] runtimeclass FontConfig { @@ -16,5 +18,8 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_FONT_SETTING(String, FontFace); INHERITABLE_FONT_SETTING(Int32, FontSize); INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight); + + INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontFeatures); + INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontAxes); } } diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 466031b05..31760ec7a 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -29,6 +29,7 @@ static constexpr std::string_view ShowTitleInTitlebarKey{ "showTerminalTitleInTi 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 UseAcrylicInTabRowKey{ "useAcrylicInTabRow" }; static constexpr std::string_view ShowTabsInTitlebarKey{ "showTabsInTitlebar" }; static constexpr std::string_view WordDelimitersKey{ "wordDelimiters" }; static constexpr std::string_view InputServiceWarningKey{ "inputServiceWarning" }; @@ -48,6 +49,8 @@ static constexpr std::string_view StartupActionsKey{ "startupActions" }; static constexpr std::string_view FocusFollowMouseKey{ "focusFollowMouse" }; static constexpr std::string_view WindowingBehaviorKey{ "windowingBehavior" }; static constexpr std::string_view TrimBlockSelectionKey{ "trimBlockSelection" }; +static constexpr std::string_view AlwaysShowTrayIconKey{ "alwaysShowTrayIcon" }; +static constexpr std::string_view MinimizeToTrayKey{ "minimizeToTray" }; static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" }; @@ -105,6 +108,7 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_Language = _Language; globals->_Theme = _Theme; globals->_TabWidthMode = _TabWidthMode; + globals->_UseAcrylicInTabRow = _UseAcrylicInTabRow; globals->_ShowTabsInTitlebar = _ShowTabsInTitlebar; globals->_WordDelimiters = _WordDelimiters; globals->_InputServiceWarning = _InputServiceWarning; @@ -129,6 +133,8 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_WindowingBehavior = _WindowingBehavior; globals->_TrimBlockSelection = _TrimBlockSelection; globals->_DetectURLs = _DetectURLs; + globals->_MinimizeToTray = _MinimizeToTray; + globals->_AlwaysShowTrayIcon = _AlwaysShowTrayIcon; globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile; globals->_validDefaultProfile = _validDefaultProfile; @@ -287,6 +293,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, TabWidthModeKey, _TabWidthMode); + JsonUtils::GetValueForKey(json, UseAcrylicInTabRowKey, _UseAcrylicInTabRow); + JsonUtils::GetValueForKey(json, SnapToGridOnResizeKey, _SnapToGridOnResize); // GetValueForKey will only override the current value if the key exists @@ -319,6 +327,10 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, DetectURLsKey, _DetectURLs); + JsonUtils::GetValueForKey(json, MinimizeToTrayKey, _MinimizeToTray); + + JsonUtils::GetValueForKey(json, AlwaysShowTrayIconKey, _AlwaysShowTrayIcon); + // This is a helper lambda to get the keybindings and commands out of both // and array of objects. We'll use this twice, once on the legacy // `keybindings` key, and again on the newer `bindings` key. @@ -400,6 +412,7 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, LanguageKey, _Language); JsonUtils::SetValueForKey(json, ThemeKey, _Theme); JsonUtils::SetValueForKey(json, TabWidthModeKey, _TabWidthMode); + JsonUtils::SetValueForKey(json, UseAcrylicInTabRowKey, _UseAcrylicInTabRow); JsonUtils::SetValueForKey(json, SnapToGridOnResizeKey, _SnapToGridOnResize); JsonUtils::SetValueForKey(json, DebugFeaturesKey, _DebugFeaturesEnabled); JsonUtils::SetValueForKey(json, ForceFullRepaintRenderingKey, _ForceFullRepaintRendering); @@ -414,6 +427,8 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, WindowingBehaviorKey, _WindowingBehavior); JsonUtils::SetValueForKey(json, TrimBlockSelectionKey, _TrimBlockSelection); JsonUtils::SetValueForKey(json, DetectURLsKey, _DetectURLs); + JsonUtils::SetValueForKey(json, MinimizeToTrayKey, _MinimizeToTray); + JsonUtils::SetValueForKey(json, AlwaysShowTrayIconKey, _AlwaysShowTrayIcon); // clang-format on json[JsonKey(ActionsKey)] = _actionMap->ToJson(); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index e11807b2a..2ac936e69 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -60,6 +60,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void UnparsedDefaultProfile(const hstring& value); void ClearUnparsedDefaultProfile(); + // TODO GH#9207: Remove this once we have a GlobalAppSettingsViewModel in TerminalSettingsEditor + void SetInvertedDisableAnimationsValue(bool invertedDisableAnimationsValue) + { + DisableAnimations(!invertedDisableAnimationsValue); + } + INHERITABLE_SETTING(Model::GlobalAppSettings, int32_t, InitialRows, DEFAULT_ROWS); INHERITABLE_SETTING(Model::GlobalAppSettings, int32_t, InitialCols, DEFAULT_COLS); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowTabs, true); @@ -68,6 +74,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation 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, UseAcrylicInTabRow, false); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ShowTabsInTitlebar, true); INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, WordDelimiters, DEFAULT_WORD_DELIMITERS); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, CopyOnSelect, false); @@ -92,6 +99,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::GlobalAppSettings, Model::WindowingMode, WindowingBehavior, Model::WindowingMode::UseNew); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, TrimBlockSelection, false); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, DetectURLs, true); + INHERITABLE_SETTING(Model::GlobalAppSettings, bool, MinimizeToTray, false); + INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowTrayIcon, false); private: guid _defaultProfile; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index fba59e1a4..a2145d391 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -36,6 +36,9 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass GlobalAppSettings { Guid DefaultProfile; + + void SetInvertedDisableAnimationsValue(Boolean invertedDisableAnimationsValue); + INHERITABLE_SETTING(String, UnparsedDefaultProfile); INHERITABLE_SETTING(Int32, InitialRows); @@ -46,6 +49,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(String, Language); INHERITABLE_SETTING(Windows.UI.Xaml.ElementTheme, Theme); INHERITABLE_SETTING(Microsoft.UI.Xaml.Controls.TabViewWidthMode, TabWidthMode); + INHERITABLE_SETTING(Boolean, UseAcrylicInTabRow); INHERITABLE_SETTING(Boolean, ShowTabsInTitlebar); INHERITABLE_SETTING(String, WordDelimiters); INHERITABLE_SETTING(Boolean, CopyOnSelect); @@ -70,6 +74,8 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(WindowingMode, WindowingBehavior); INHERITABLE_SETTING(Boolean, TrimBlockSelection); INHERITABLE_SETTING(Boolean, DetectURLs); + INHERITABLE_SETTING(Boolean, MinimizeToTray); + INHERITABLE_SETTING(Boolean, AlwaysShowTrayIcon); Windows.Foundation.Collections.IMapView ColorSchemes(); void AddColorScheme(ColorScheme scheme); diff --git a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl index 2b5cbbd52..b33cf23e7 100644 --- a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl +++ b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl @@ -22,6 +22,14 @@ namespace Microsoft.Terminal.Settings.Model Vertical_Bottom = 0x20 }; + [flags] + enum IntenseStyle + { + Bold = 0x1, + Bright = 0x2, + All = 0xffffffff + }; + interface IAppearanceConfig { Microsoft.Terminal.Settings.Model.Profile SourceProfile { get; }; @@ -42,5 +50,6 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_APPEARANCE_SETTING(Boolean, RetroTerminalEffect); INHERITABLE_APPEARANCE_SETTING(String, PixelShaderPath); + INHERITABLE_APPEARANCE_SETTING(IntenseStyle, IntenseTextStyle); }; } diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index efb3c0764..220ec1df3 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -101,7 +101,7 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils { public: DeserializationError(const Json::Value& value) : - runtime_error("failed to deserialize"), + runtime_error(std::string("failed to deserialize ") + (value.isNull() ? "" : value.asCString())), jsonValue{ value } {} void SetKey(std::string_view newKey) @@ -177,6 +177,58 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils } }; + template + struct ConversionTrait> + { + std::unordered_map FromJson(const Json::Value& json) const + { + std::unordered_map val; + val.reserve(json.size()); + + ConversionTrait trait; + for (auto it = json.begin(), end = json.end(); it != end; ++it) + { + GetValue(*it, val[it.name()], trait); + } + + return val; + } + + bool CanConvert(const Json::Value& json) const + { + if (!json.isObject()) + { + return false; + } + ConversionTrait trait; + for (const auto& v : json) + { + if (!trait.CanConvert(v)) + { + return false; + } + } + return true; + } + + Json::Value ToJson(const std::unordered_map& val) + { + Json::Value json{ Json::objectValue }; + + for (const auto& [k, v] : val) + { + SetValueForKey(json, k, v); + } + + return json; + } + + std::string TypeDescription() const + { + return fmt::format("map (string, {})", ConversionTrait{}.TypeDescription()); + } + }; + #ifdef WINRT_BASE_H template<> struct ConversionTrait : public ConversionTrait @@ -206,6 +258,58 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils return ConversionTrait::CanConvert(json) || json.isNull(); } }; + + template + struct ConversionTrait> + { + winrt::Windows::Foundation::Collections::IMap FromJson(const Json::Value& json) const + { + std::unordered_map val; + val.reserve(json.size()); + + ConversionTrait trait; + for (auto it = json.begin(), end = json.end(); it != end; ++it) + { + GetValue(*it, val[winrt::to_hstring(it.name())], trait); + } + + return winrt::single_threaded_map(std::move(val)); + } + + bool CanConvert(const Json::Value& json) const + { + if (!json.isObject()) + { + return false; + } + ConversionTrait trait; + for (const auto& v : json) + { + if (!trait.CanConvert(v)) + { + return false; + } + } + return true; + } + + Json::Value ToJson(const winrt::Windows::Foundation::Collections::IMap& val) + { + Json::Value json{ Json::objectValue }; + + for (const auto& [k, v] : val) + { + SetValueForKey(json, til::u16u8(k), v); + } + + return json; + } + + std::string TypeDescription() const + { + return fmt::format("map (string, {})", ConversionTrait{}.TypeDescription()); + } + }; #endif template<> diff --git a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp index 0cc62460c..378f92bde 100644 --- a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp @@ -17,6 +17,8 @@ constexpr std::wstring_view SHIFT_KEY{ L"shift" }; constexpr std::wstring_view ALT_KEY{ L"alt" }; constexpr std::wstring_view WIN_KEY{ L"win" }; +// If you modify this list you should modify the +// KeyChordSegment description in profiles.schema.json. #define VKEY_NAME_PAIRS(XX) \ XX(VK_RETURN, L"enter") \ XX(VK_TAB, L"tab") \ @@ -73,7 +75,40 @@ constexpr std::wstring_view WIN_KEY{ L"win" }; XX(VK_NUMPAD7, L"numpad7", L"numpad_7") \ XX(VK_NUMPAD8, L"numpad8", L"numpad_8") \ XX(VK_NUMPAD9, L"numpad9", L"numpad_9") \ - XX(VK_OEM_PLUS, L"plus") + XX(VK_OEM_PLUS, L"plus") /* '+' any country */ \ + XX(VK_OEM_COMMA, L"comma") /* ',' any country */ \ + XX(VK_OEM_MINUS, L"minus") /* '-' any country */ \ + XX(VK_OEM_PERIOD, L"period") /* '.' any country */ \ + XX(VK_BROWSER_BACK, L"browser_back") \ + XX(VK_BROWSER_FORWARD, L"browser_forward") \ + XX(VK_BROWSER_REFRESH, L"browser_refresh") \ + XX(VK_BROWSER_STOP, L"browser_stop") \ + XX(VK_BROWSER_SEARCH, L"browser_search") \ + XX(VK_BROWSER_FAVORITES, L"browser_favorites") \ + XX(VK_BROWSER_HOME, L"browser_home") + +constexpr std::wstring_view vkeyPrefix{ L"vk(" }; +constexpr std::wstring_view scanCodePrefix{ L"sc(" }; +constexpr std::wstring_view codeSuffix{ L")" }; + +// Parses a vk(nnn) or sc(nnn) key chord part. +// If the part doesn't contain either of these two this function returns 0. +// For invalid arguments we throw an exception. +static int32_t parseNumericCode(const std::wstring_view& str, const std::wstring_view& prefix, const std::wstring_view& suffix) +{ + if (!til::ends_with(str, suffix) || !til::starts_with(str, prefix)) + { + return 0; + } + + const auto value = til::from_wchars({ str.data() + prefix.size(), str.size() - prefix.size() - suffix.size() }); + if (value > 0 && value < 256) + { + return gsl::narrow_cast(value); + } + + throw winrt::hresult_invalid_argument(L"Invalid numeric argument to vk() or sc()"); +} // Function Description: // - Deserializes the given string into a new KeyChord instance. If this @@ -109,6 +144,7 @@ static KeyChord _fromString(std::wstring_view wstr) VirtualKeyModifiers modifiers = VirtualKeyModifiers::None; int32_t vkey = 0; + int32_t scanCode = 0; while (!wstr.empty()) { @@ -132,10 +168,9 @@ static KeyChord _fromString(std::wstring_view wstr) } else { - if (vkey) + if (vkey || scanCode) { - // Key bindings like Ctrl+A+B are not valid. - throw winrt::hresult_invalid_argument(); + throw winrt::hresult_invalid_argument(L"Key bindings like Ctrl+A+B are not valid"); } // Characters 0-9, a-z, A-Z directly map to virtual keys. @@ -149,6 +184,22 @@ static KeyChord _fromString(std::wstring_view wstr) } } + // vk() allows a user to specify a virtual key code + // and sc() allows them to specify a scan code manually. + // + // ctrl+vk(0x09) for instance is the same as ctrl+tab, while win+sc(41) specifies + // a key binding which is (seemingly) always bound to the key below Esc. + vkey = parseNumericCode(part, vkeyPrefix, codeSuffix); + if (vkey) + { + continue; + } + scanCode = parseNumericCode(part, scanCodePrefix, codeSuffix); + if (scanCode) + { + continue; + } + // nameToVkey contains a few more mappings like "F11". if (const auto it = nameToVkey.find(part); it != nameToVkey.end()) { @@ -172,11 +223,11 @@ static KeyChord _fromString(std::wstring_view wstr) } } - throw winrt::hresult_invalid_argument(); + throw winrt::hresult_invalid_argument(L"Invalid key binding"); } } - return KeyChord{ modifiers, vkey }; + return KeyChord{ modifiers, vkey, scanCode }; } // Function Description: @@ -204,6 +255,7 @@ static std::wstring _toString(const KeyChord& chord) const auto modifiers = chord.Modifiers(); const auto vkey = chord.Vkey(); + const auto scanCode = chord.ScanCode(); std::wstring buffer; // Add modifiers @@ -228,6 +280,14 @@ static std::wstring _toString(const KeyChord& chord) buffer.push_back(L'+'); } + if (scanCode) + { + buffer.append(scanCodePrefix); + buffer.append(std::to_wstring(scanCode)); + buffer.append(codeSuffix); + return buffer; + } + // Quick lookup: ranges of vkeys that correlate directly to a key. if ((vkey >= L'0' && vkey <= L'9') || (vkey >= L'A' && vkey <= L'Z')) { @@ -248,6 +308,14 @@ static std::wstring _toString(const KeyChord& chord) return buffer; } + if (vkey) + { + buffer.append(vkeyPrefix); + buffer.append(std::to_wstring(vkey)); + buffer.append(codeSuffix); + return buffer; + } + return {}; } diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index 202f07808..5b9dc9ef3 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -235,12 +235,12 @@ - + 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}. - + x86 $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.2.5.0-prerelease.201202003\runtimes\win10-$(Native-Platform)\native\" + <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.2.6.2-prerelease.210818003\runtimes\win10-$(Native-Platform)\native\" diff --git a/src/features.xml b/src/features.xml index 42ff2e5f9..c50399f4a 100644 --- a/src/features.xml +++ b/src/features.xml @@ -64,6 +64,13 @@ + + Feature_TrayIcon + Controls whether the Tray Icon and related settings (aka. MinimizeToTray and AlwaysShowTrayIcon) are enabled + AlwaysEnabled + + + Feature_ShowProfileDefaultsInSettings Whether to show the "defaults" page in the Terminal settings UI diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 917501dba..e00238e41 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -28,8 +28,6 @@ using Microsoft::Console::VirtualTerminal::StateMachine; // Used by WriteCharsLegacy. #define IS_GLYPH_CHAR(wch) (((wch) >= L' ') && ((wch) != 0x007F)) -constexpr unsigned int LOCAL_BUFFER_SIZE = 100; - // Routine Description: // - This routine updates the cursor position. Its input is the non-special // cased new location of the cursor. For example, if the cursor were being @@ -339,7 +337,6 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; COORD CursorPosition = cursor.GetPosition(); NTSTATUS Status = STATUS_SUCCESS; SHORT XPosition; - WCHAR LocalBuffer[LOCAL_BUFFER_SIZE]; size_t TempNumSpaces = 0; const bool fUnprocessed = WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT); const bool fWrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); @@ -360,6 +357,9 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y); } + static constexpr unsigned int LOCAL_BUFFER_SIZE = 1024; + WCHAR LocalBuffer[LOCAL_BUFFER_SIZE]; + while (*pcb < BufferSize) { // correct for delayed EOL diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index 81ef9faa7..112905921 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -259,11 +259,12 @@ COLORREF CONSOLE_INFORMATION::GetDefaultBackground() const noexcept std::pair CONSOLE_INFORMATION::LookupAttributeColors(const TextAttribute& attr) const noexcept { _blinkingState.RecordBlinkingUsage(attr); - return attr.CalculateRgbColors(Get256ColorTable(), - GetDefaultForeground(), - GetDefaultBackground(), - IsScreenReversed(), - _blinkingState.IsBlinkingFaint()); + return attr.CalculateRgbColors( + GetColorTable(), + GetDefaultForeground(), + GetDefaultBackground(), + IsScreenReversed(), + _blinkingState.IsBlinkingFaint()); } // Method Description: diff --git a/src/host/getset.cpp b/src/host/getset.cpp index dc0459f6d..9c3ccde44 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -1639,6 +1639,30 @@ void DoSrvEndHyperlink(SCREEN_INFORMATION& screenInfo) screenInfo.GetTextBuffer().SetCurrentAttributes(attr); } +// Routine Description: +// - A private API call for updating the active soft font. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - S_OK if we succeeded, otherwise the HRESULT of the failure. +[[nodiscard]] HRESULT DoSrvUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + try + { + auto* pRender = ServiceLocator::LocateGlobals().pRender; + if (pRender) + { + pRender->UpdateSoftFont(bitPattern, cellSize, centeringHint); + } + return S_OK; + } + CATCH_RETURN(); +} + // Routine Description: // - A private API call for forcing the renderer to repaint the screen. If the // input screen buffer is not the active one, then just do nothing. We only @@ -1931,15 +1955,11 @@ void DoSrvPrivateRefreshWindow(_In_ const SCREEN_INFORMATION& screenInfo) // to embed control characters in that string. if (gci.IsInVtIoMode()) { - std::wstring sanitized; - sanitized.reserve(title.size()); - for (size_t i = 0; i < title.size(); i++) - { - if (title.at(i) >= UNICODE_SPACE) - { - sanitized.push_back(title.at(i)); - } - } + std::wstring sanitized{ title }; + sanitized.erase(std::remove_if(sanitized.begin(), sanitized.end(), [](auto ch) { + return ch < UNICODE_SPACE || (ch > UNICODE_DEL && ch < UNICODE_NBSP); + }), + sanitized.end()); gci.SetTitle({ sanitized }); } diff --git a/src/host/getset.h b/src/host/getset.h index 43b8d5fef..1fb48e286 100644 --- a/src/host/getset.h +++ b/src/host/getset.h @@ -55,6 +55,10 @@ void DoSrvAddHyperlink(SCREEN_INFORMATION& screenInfo, void DoSrvEndHyperlink(SCREEN_INFORMATION& screenInfo); +[[nodiscard]] HRESULT DoSrvUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept; + void DoSrvPrivateRefreshWindow(const SCREEN_INFORMATION& screenInfo); [[nodiscard]] HRESULT DoSrvSetConsoleOutputCodePage(const unsigned int codepage); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 3129f0a1a..a70b48afd 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -818,3 +818,18 @@ bool ConhostInternalGetSet::PrivateEndHyperlink() const DoSrvEndHyperlink(_io.GetActiveOutputBuffer()); return true; } + +// Routine Description: +// - Replaces the active soft font with the given bit pattern. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - true if successful (see DoSrvUpdateSoftFont). false otherwise. +bool ConhostInternalGetSet::PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + return SUCCEEDED(DoSrvUpdateSoftFont(bitPattern, cellSize, centeringHint)); +} diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 34048e026..99889baca 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -147,6 +147,10 @@ public: bool PrivateAddHyperlink(const std::wstring_view uri, const std::wstring_view params) const override; bool PrivateEndHyperlink() const override; + bool PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; + private: Microsoft::Console::IIoProvider& _io; }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 81bbfd391..5323cb574 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1904,10 +1904,17 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const ppsiNewScreenBuffer); if (NT_SUCCESS(Status)) { - // Update the alt buffer's cursor style to match our own. + // Update the alt buffer's cursor style, visibility, and position to match our own. auto& myCursor = GetTextBuffer().GetCursor(); auto* const createdBuffer = *ppsiNewScreenBuffer; - createdBuffer->GetTextBuffer().GetCursor().SetStyle(myCursor.GetSize(), myCursor.GetColor(), myCursor.GetType()); + auto& altCursor = createdBuffer->GetTextBuffer().GetCursor(); + altCursor.SetStyle(myCursor.GetSize(), myCursor.GetColor(), myCursor.GetType()); + altCursor.SetIsVisible(myCursor.IsVisible()); + altCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed()); + // The new position should match the viewport-relative position of the main buffer. + auto altCursorPos = myCursor.GetPosition(); + altCursorPos.Y -= GetVirtualViewport().Top(); + altCursor.SetPosition(altCursorPos); s_InsertScreenBuffer(createdBuffer); @@ -1995,6 +2002,14 @@ void SCREEN_INFORMATION::UseMainScreenBuffer() SCREEN_INFORMATION* psiAlt = psiMain->_psiAlternateBuffer; psiMain->_psiAlternateBuffer = nullptr; + + // Copy the alt buffer's cursor style and visibility back to the main buffer. + const auto& altCursor = psiAlt->GetTextBuffer().GetCursor(); + auto& mainCursor = psiMain->GetTextBuffer().GetCursor(); + mainCursor.SetStyle(altCursor.GetSize(), altCursor.GetColor(), altCursor.GetType()); + mainCursor.SetIsVisible(altCursor.IsVisible()); + mainCursor.SetBlinkingAllowed(altCursor.IsBlinkingAllowed()); + s_RemoveScreenBuffer(psiAlt); // this will also delete the alt buffer // deleting the alt buffer will give the GetSet back to its main diff --git a/src/host/settings.cpp b/src/host/settings.cpp index af1de09e3..a9fb5d093 100644 --- a/src/host/settings.cpp +++ b/src/host/settings.cpp @@ -726,16 +726,6 @@ void Settings::SetHistoryNoDup(const bool bHistoryNoDup) _bHistoryNoDup = bHistoryNoDup; } -gsl::span Settings::Get16ColorTable() const -{ - return Get256ColorTable().subspan(0, 16); -} - -gsl::span Settings::Get256ColorTable() const -{ - return { _colorTable.data(), _colorTable.size() }; -} - void Settings::SetColorTableEntry(const size_t index, const COLORREF ColorValue) { _colorTable.at(index) = ColorValue; diff --git a/src/host/settings.hpp b/src/host/settings.hpp index 25eb06dac..e8468da6b 100644 --- a/src/host/settings.hpp +++ b/src/host/settings.hpp @@ -159,8 +159,12 @@ public: bool GetHistoryNoDup() const; void SetHistoryNoDup(const bool fHistoryNoDup); - gsl::span Get16ColorTable() const; - gsl::span Get256ColorTable() const; + // The first 16 items of the color table are the same as the 16-color palette. + inline const std::array& GetColorTable() const noexcept + { + return _colorTable; + } + void SetColorTableEntry(const size_t index, const COLORREF ColorValue); COLORREF GetColorTableEntry(const size_t index) const; diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp index 5aaab774e..4d17ddfe1 100644 --- a/src/host/srvinit.cpp +++ b/src/host/srvinit.cpp @@ -336,8 +336,27 @@ HRESULT ConsoleCreateIoThread(_In_ HANDLE Server, RETURN_IF_FAILED(g.pDeviceComm->SetServerInformation(&ServerInformation)); } + // Ensure that whatever we're giving to the new thread is on the heap so it cannot + // go out of scope by the time that thread starts. + // (e.g. if someone sent us a pointer to stack memory... that could happen + // ask me how I know... :| ) + std::unique_ptr heapConnectMessage; + if (connectMessage) + { + // Allocate and copy onto the heap + heapConnectMessage = std::make_unique(*connectMessage); + + // Set the pointer that `CreateThread` uses to the heap space + connectMessage = heapConnectMessage.get(); + } + HANDLE const hThread = CreateThread(nullptr, 0, ConsoleIoThread, connectMessage, 0, nullptr); RETURN_HR_IF(E_HANDLE, hThread == nullptr); + + // If we successfully started the other thread, it's that guy's problem to free the connect message. + // (If we didn't make one, it should be no problem to release the empty unique_ptr.) + heapConnectMessage.release(); + LOG_IF_FAILED(SetThreadDescription(hThread, L"Console Driver Message IO Thread")); LOG_IF_WIN32_BOOL_FALSE(CloseHandle(hThread)); // The thread will run on its own and close itself. Free the associated handle. @@ -871,7 +890,11 @@ DWORD WINAPI ConsoleIoThread(LPVOID lpParameter) // If we were given a message on startup, process that in our context and then continue with the IO loop normally. if (lpParameter) { - ReceiveMsg = *(PCONSOLE_API_MSG)lpParameter; + // Capture the incoming lpParameter into a unique_ptr so we can appropriately + // free the heap memory when we're done getting the important bits out of it below. + std::unique_ptr capturedMessage{ static_cast(lpParameter) }; + + ReceiveMsg = *capturedMessage.get(); ReceiveMsg._pApiRoutines = &globals.api; ReceiveMsg._pDeviceComm = globals.pDeviceComm; IoSorter::ServiceIoOperation(&ReceiveMsg, &ReplyMsg); diff --git a/src/host/stream.cpp b/src/host/stream.cpp index 12806de01..6f642ddd9 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -706,8 +706,6 @@ size_t RetrieveNumberOfSpaces(_In_ SHORT sOriginalCursorPositionX, return STATUS_BUFFER_TOO_SMALL; } - const size_t OutputBufferSize = buffer.size_bytes(); - if (readHandleState.IsInputPending()) { return _ReadPendingInput(inputBuffer, diff --git a/src/host/telemetry.cpp b/src/host/telemetry.cpp index 59080d3a9..4888b38eb 100644 --- a/src/host/telemetry.cpp +++ b/src/host/telemetry.cpp @@ -419,7 +419,7 @@ void Telemetry::WriteFinalTraceLog() TraceLoggingBool(gci.GetQuickEdit(), "QuickEdit"), TraceLoggingValue(gci.GetWindowAlpha(), "WindowAlpha"), TraceLoggingBool(gci.GetWrapText(), "WrapText"), - TraceLoggingUInt32Array((UINT32 const*)gci.Get16ColorTable().data(), (UINT16)gci.Get16ColorTable().size(), "ColorTable"), + TraceLoggingUInt32Array((UINT32 const*)gci.GetColorTable().data(), 16, "ColorTable"), TraceLoggingValue(gci.CP, "CodePageInput"), TraceLoggingValue(gci.OutputCP, "CodePageOutput"), TraceLoggingValue(gci.GetFontSize().X, "FontSizeX"), @@ -453,7 +453,7 @@ void Telemetry::WriteFinalTraceLog() TraceLoggingValue(gci.GetShowWindow(), "ShowWindow"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - static_assert(sizeof(UINT32) == sizeof(gci.Get16ColorTable()[0]), "gci.Get16ColorTable()"); + static_assert(sizeof(UINT32) == sizeof(gci.GetColorTable()[0]), "gci.Get16ColorTable()"); // I could use the TraceLoggingUIntArray, but then we would have to know the order of the enums on the backend. // So just log each enum count separately with its string representation which makes it more human readable. diff --git a/src/host/tracing.cpp b/src/host/tracing.cpp index ccd8716c7..111436ce1 100644 --- a/src/host/tracing.cpp +++ b/src/host/tracing.cpp @@ -407,9 +407,9 @@ void Tracing::s_TraceInputRecord(const INPUT_RECORD& inputRecord) } void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_reads_(cchCookedBufferLength) const wchar_t* pwchCookedBuffer, _In_ ULONG cchCookedBufferLength) -{ - if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::CookedRead)) { - +{ + if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::CookedRead)) + { TraceLoggingWrite( g_hConhostV2EventTraceProvider, "CookedRead", @@ -423,9 +423,9 @@ void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcess } void Tracing::s_TraceConsoleAttachDetach(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_ bool bIsAttach) -{ - if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::ConsoleAttachDetach)) { - +{ + if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::ConsoleAttachDetach)) + { bool bIsUserInteractive = Telemetry::Instance().IsUserInteractive(); TraceLoggingWrite( diff --git a/src/host/ut_host/ApiRoutinesTests.cpp b/src/host/ut_host/ApiRoutinesTests.cpp index cf0e8cd18..a8dfcf4c1 100644 --- a/src/host/ut_host/ApiRoutinesTests.cpp +++ b/src/host/ut_host/ApiRoutinesTests.cpp @@ -288,7 +288,7 @@ class ApiRoutinesTests nullptr, nullptr); - wistd::unique_ptr pszExpected = wil::make_unique_nothrow(iBytesNeeded); + wistd::unique_ptr pszExpected = wil::make_unique_nothrow(iBytesNeeded + 1); VERIFY_IS_NOT_NULL(pszExpected); VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP, @@ -300,6 +300,9 @@ class ApiRoutinesTests nullptr, nullptr)); + // Make sure we terminate the expected title -- WC2MB does not add the \0 if we use the size variant + pszExpected[iBytesNeeded] = '\0'; + char pszTitle[MAX_PATH]; // most applications use MAX_PATH size_t cchWritten = 0; size_t cchNeeded = 0; diff --git a/src/host/ut_host/ConptyOutputTests.cpp b/src/host/ut_host/ConptyOutputTests.cpp index a76d5c4f8..d412fb562 100644 --- a/src/host/ut_host/ConptyOutputTests.cpp +++ b/src/host/ut_host/ConptyOutputTests.cpp @@ -119,6 +119,7 @@ class ConptyOutputTests TEST_METHOD(WriteTwoLinesUsesNewline); TEST_METHOD(WriteAFewSimpleLines); TEST_METHOD(InvalidateUntilOneBeforeEnd); + TEST_METHOD(SetConsoleTitleWithControlChars); private: bool _writeCallback(const char* const pch, size_t const cch); @@ -364,3 +365,37 @@ void ConptyOutputTests::InvalidateUntilOneBeforeEnd() VERIFY_SUCCEEDED(renderer.PaintFrame()); } + +void ConptyOutputTests::SetConsoleTitleWithControlChars() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:control", L"{0x00, 0x0A, 0x1B, 0x80, 0x9B, 0x9C}") + END_TEST_METHOD_PROPERTIES() + + int control; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"control", control)); + + auto& g = ServiceLocator::LocateGlobals(); + auto& renderer = *g.pRender; + + Log::Comment(NoThrowString().Format( + L"SetConsoleTitle with a control character (0x%02X) embedded in the text", control)); + + std::wstringstream titleText; + titleText << L"Hello " << wchar_t(control) << L"World!"; + VERIFY_SUCCEEDED(DoSrvSetConsoleTitleW(titleText.str())); + + // This is the standard init sequences for the first frame. + expectedOutput.push_back("\x1b[2J"); + expectedOutput.push_back("\x1b[m"); + expectedOutput.push_back("\x1b[H"); + + // The title change is propagated as an OSC 0 sequence. + // Control characters are stripped, so it's always "Hello World". + expectedOutput.push_back("\x1b]0;Hello World!\a"); + + // This is also part of the standard init sequence. + expectedOutput.push_back("\x1b[?25h"); + + VERIFY_SUCCEEDED(renderer.PaintFrame()); +} diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index d8f2359db..0e31c2b32 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -89,6 +89,8 @@ class ScreenBufferTests TEST_METHOD(MultipleAlternateBuffersFromMainCreationTest); + TEST_METHOD(AlternateBufferCursorInheritanceTest); + TEST_METHOD(TestReverseLineFeed); TEST_METHOD(TestResetClearTabStops); @@ -344,6 +346,71 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest() } } +void ScreenBufferTests::AlternateBufferCursorInheritanceTest() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + auto& mainBuffer = gci.GetActiveOutputBuffer(); + auto& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); + + Log::Comment(L"Set the cursor attributes in the main buffer."); + auto mainCursorPos = COORD{ 3, 5 }; + auto mainCursorVisible = false; + auto mainCursorSize = 33u; + auto mainCursorColor = RGB(1, 2, 3); + auto mainCursorType = CursorType::DoubleUnderscore; + auto mainCursorBlinking = false; + mainCursor.SetPosition(mainCursorPos); + mainCursor.SetIsVisible(mainCursorVisible); + mainCursor.SetStyle(mainCursorSize, mainCursorColor, mainCursorType); + mainCursor.SetBlinkingAllowed(mainCursorBlinking); + + Log::Comment(L"Switch to the alternate buffer."); + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + auto& altBuffer = gci.GetActiveOutputBuffer(); + auto& altCursor = altBuffer.GetTextBuffer().GetCursor(); + auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); + + Log::Comment(L"Confirm the cursor position is inherited from the main buffer."); + VERIFY_ARE_EQUAL(mainCursorPos, altCursor.GetPosition()); + Log::Comment(L"Confirm the cursor visibility is inherited from the main buffer."); + VERIFY_ARE_EQUAL(mainCursorVisible, altCursor.IsVisible()); + Log::Comment(L"Confirm the cursor style is inherited from the main buffer."); + VERIFY_ARE_EQUAL(mainCursorSize, altCursor.GetSize()); + VERIFY_ARE_EQUAL(mainCursorColor, altCursor.GetColor()); + VERIFY_ARE_EQUAL(mainCursorType, altCursor.GetType()); + VERIFY_ARE_EQUAL(mainCursorBlinking, altCursor.IsBlinkingAllowed()); + + Log::Comment(L"Set the cursor attributes in the alt buffer."); + auto altCursorPos = COORD{ 5, 3 }; + auto altCursorVisible = true; + auto altCursorSize = 66u; + auto altCursorColor = RGB(3, 2, 1); + auto altCursorType = CursorType::EmptyBox; + auto altCursorBlinking = true; + altCursor.SetPosition(altCursorPos); + altCursor.SetIsVisible(altCursorVisible); + altCursor.SetStyle(altCursorSize, altCursorColor, altCursorType); + altCursor.SetBlinkingAllowed(altCursorBlinking); + + Log::Comment(L"Switch back to the main buffer."); + useMain.release(); + altBuffer.UseMainScreenBuffer(); + VERIFY_ARE_EQUAL(&mainBuffer, &gci.GetActiveOutputBuffer()); + + Log::Comment(L"Confirm the cursor position is restored to what it was."); + VERIFY_ARE_EQUAL(mainCursorPos, mainCursor.GetPosition()); + Log::Comment(L"Confirm the cursor visibility is inherited from the alt buffer."); + VERIFY_ARE_EQUAL(altCursorVisible, mainCursor.IsVisible()); + Log::Comment(L"Confirm the cursor style is inherited from the alt buffer."); + VERIFY_ARE_EQUAL(altCursorSize, mainCursor.GetSize()); + VERIFY_ARE_EQUAL(altCursorColor, mainCursor.GetColor()); + VERIFY_ARE_EQUAL(altCursorType, mainCursor.GetType()); + VERIFY_ARE_EQUAL(altCursorBlinking, mainCursor.IsBlinkingAllowed()); +} + void ScreenBufferTests::TestReverseLineFeed() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -4948,6 +5015,9 @@ void ScreenBufferTests::ClearAlternateBuffer() auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); + // Set the position to home, otherwise it's inherited from the main buffer. + VERIFY_SUCCEEDED(altBuffer.SetCursorPosition({ 0, 0 }, true)); + WriteText(altBuffer.GetTextBuffer()); VerifyText(altBuffer.GetTextBuffer()); diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index b17853805..a12dd8673 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -420,6 +420,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;5;6;7m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x00030201, 0x00070605 }, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -428,6 +429,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;7;8;9m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x00030201, 0x00090807 }, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -435,6 +437,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;2;10;11;12m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x000c0b0a, 0x00090807 }, &renderData, + false, false)); }); @@ -444,6 +447,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x000c0b0a, 0x00090807 }, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -458,6 +462,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -469,6 +474,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -477,6 +483,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -485,6 +492,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;19;161;14m"); // Background RGB(19,161,14) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -493,6 +501,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;2;193;156;0m"); // Foreground RGB(193,156,0) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -501,6 +510,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[49m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -509,6 +519,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;5;7m"); // Foreground DARK_WHITE (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -517,6 +528,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;5;1m"); // Background DARK_RED (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -525,6 +537,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[39m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -533,6 +546,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); }); @@ -542,6 +556,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -795,7 +810,7 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { @@ -841,29 +856,29 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() break; } qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[32m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[42m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } @@ -1081,6 +1096,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -1092,6 +1108,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1100,6 +1117,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1108,6 +1126,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[42m"); // Background DARK_GREEN VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1116,6 +1135,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[33m"); // Foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1125,6 +1145,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[33m"); // Reapply foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1133,6 +1154,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1141,6 +1163,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1150,6 +1173,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Reapply background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1158,6 +1182,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); }); @@ -1167,6 +1192,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -1304,7 +1330,7 @@ void VtRendererTest::XtermTestAttributesAcrossReset() Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { @@ -1322,29 +1348,29 @@ void VtRendererTest::XtermTestAttributesAcrossReset() break; } qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[32m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[42m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } diff --git a/src/inc/til.h b/src/inc/til.h index 509f02db9..3202a5959 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -89,7 +89,9 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } \ } while (0, 0) -// Due to a bug (DevDiv 441931), Warning 4297 (function marked noexcept throws exception) is detected even when the throwing code is unreachable, such as the end of scope after a return, in function-level catch. +// Due to a bug (DevDiv 441931), Warning 4297 (function marked noexcept throws +// exception) is detected even when the throwing code is unreachable, such as +// the end of scope after a return, in function-level catch. #define CATCH_LOG_RETURN_FALSE() \ catch (...) \ { \ @@ -98,6 +100,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return false; \ } +// This is like the above, but doesn't log any messages. This is for GH#10882. +#define CATCH_RETURN_FALSE() \ + catch (...) \ + { \ + return false; \ + } + // MultiByteToWideChar has a bug in it where it can return 0 and then not set last error. // WIL has a fit if the last error is 0 when a bool false is returned. // This macro doesn't have a fit. It just reports E_UNEXPECTED instead. diff --git a/src/inc/til/point.h b/src/inc/til/point.h index 366ba1807..5d4758825 100644 --- a/src/inc/til/point.h +++ b/src/inc/til/point.h @@ -338,6 +338,21 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } #endif +#ifdef WINRT_Microsoft_Terminal_Core_H + constexpr point(const winrt::Microsoft::Terminal::Core::Point& corePoint) : + point(corePoint.X, corePoint.Y) + { + } + + operator winrt::Microsoft::Terminal::Core::Point() const + { + winrt::Microsoft::Terminal::Core::Point ret; + ret.X = x(); + ret.Y = y(); + return ret; + } +#endif + std::wstring to_string() const { return wil::str_printf(L"(X:%td, Y:%td)", x(), y()); diff --git a/src/inc/til/rectangle.h b/src/inc/til/rectangle.h index 83bb0b3f1..43bae773f 100644 --- a/src/inc/til/rectangle.h +++ b/src/inc/til/rectangle.h @@ -183,6 +183,14 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { } + // This template will convert to rectangle from anything that has a X, Y, Width, and Height field that are floating-point; + // a math type is required. + template + constexpr rectangle(TilMath, const TOther& other, std::enable_if_t().X)> && std::is_floating_point_v().Y)> && std::is_floating_point_v().Width)> && std::is_floating_point_v().Height)>, int> /*sentinel*/ = 0) : + rectangle(til::point{ TilMath::template cast(other.X), TilMath::template cast(other.Y) }, til::size{ TilMath::template cast(other.Width), TilMath::template cast(other.Height) }) + { + } + // This template will convert to rectangle from anything that has a left, top, right, and bottom field that are floating-point; // a math type is required. template @@ -875,6 +883,22 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } #endif +#ifdef WINRT_Microsoft_Terminal_Core_H + operator winrt::Microsoft::Terminal::Core::Padding() const + { + winrt::Microsoft::Terminal::Core::Padding ret; + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(left()).AssignIfValid(&ret.Left)); + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(top()).AssignIfValid(&ret.Top)); + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(right()).AssignIfValid(&ret.Right)); + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(bottom()).AssignIfValid(&ret.Bottom)); + return ret; + } + constexpr rectangle(const winrt::Microsoft::Terminal::Core::Padding& padding) : + rectangle(til::math::rounding, padding) + { + } +#endif + std::wstring to_string() const { return wil::str_printf(L"(L:%td, T:%td, R:%td, B:%td) [W:%td, H:%td]", left(), top(), right(), bottom(), width(), height()); diff --git a/src/inc/til/string.h b/src/inc/til/string.h index 4bcbf3d5e..13299e73e 100644 --- a/src/inc/til/string.h +++ b/src/inc/til/string.h @@ -73,6 +73,77 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return ends_with<>(str, prefix); } + inline constexpr unsigned long from_wchars_error = ULONG_MAX; + + // Just like std::wcstoul, but without annoying locales and null-terminating strings. + // It has been fuzz-tested against clang's strtoul implementation. + _TIL_INLINEPREFIX unsigned long from_wchars(const std::wstring_view& str) noexcept + { + static constexpr unsigned long maximumValue = ULONG_MAX / 16; + + // We don't have to test ptr for nullability, as we only access it under either condition: + // * str.length() > 0, for determining the base + // * ptr != end, when parsing the characters; if ptr is null, length will be 0 and thus end == ptr +#pragma warning(push) +#pragma warning(disable : 26429) // Symbol 'ptr' is never tested for nullness, it can be marked as not_null +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead + auto ptr = str.data(); + const auto end = ptr + str.length(); + unsigned long base = 10; + unsigned long accumulator = 0; + unsigned long value = ULONG_MAX; + + if (str.length() > 1 && *ptr == L'0') + { + base = 8; + ptr++; + + if (str.length() > 2 && (*ptr == L'x' || *ptr == L'X')) + { + base = 16; + ptr++; + } + } + + if (ptr == end) + { + return from_wchars_error; + } + + for (;; accumulator *= base) + { + value = ULONG_MAX; + if (*ptr >= L'0' && *ptr <= L'9') + { + value = *ptr - L'0'; + } + else if (*ptr >= L'A' && *ptr <= L'F') + { + value = *ptr - L'A' + 10; + } + else if (*ptr >= L'a' && *ptr <= L'f') + { + value = *ptr - L'a' + 10; + } + else + { + return from_wchars_error; + } + + accumulator += value; + if (accumulator >= maximumValue) + { + return from_wchars_error; + } + + if (++ptr == end) + { + return accumulator; + } + } +#pragma warning(pop) + } + // Just like std::tolower, but without annoying locales. template constexpr T tolower_ascii(T c) diff --git a/src/inc/til/u8u16convert.h b/src/inc/til/u8u16convert.h index af6e4f9b7..226ae5ba0 100644 --- a/src/inc/til/u8u16convert.h +++ b/src/inc/til/u8u16convert.h @@ -16,249 +16,36 @@ Based on the results the decision was made to keep using the platform functions MultiByteToWideChar and WideCharToMultiByte. Author(s): -- Steffen Illhardt (german-one) 2020 +- Steffen Illhardt (german-one), Leonard Hecker (lhecker) 2020-2021 --*/ #pragma once namespace til // Terminal Implementation Library. Also: "Today I Learned" { - template - class u8u16state final + // state structure for maintenance of UTF-8 partials + struct u8state { - public: - u8u16state() noexcept : - _buffer{}, - _utfPartials{} + char partials[4]; + uint8_t have{}; + uint8_t want{}; + + constexpr void reset() noexcept { + *this = {}; } - - // Method Description: - // - Takes a UTF-8 string and populates it with *complete* UTF-8 codepoints. - // If it receives an incomplete codepoint, it will cache it until it can be completed. - // Arguments: - // - in - UTF-8 string_view potentially containing partial code points - // - out - on return, populated with complete codepoints at the string end - // Return Value: - // - S_OK - the resulting string doesn't end with a partial - // - S_FALSE - the resulting string contains the previously cached partials only - // - E_OUTOFMEMORY - the method failed to allocate memory for the resulting string - // - E_ABORT - the resulting string length would exceed the max_size and thus, the processing was aborted - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value, HRESULT>::type - operator()(const std::basic_string_view in, std::basic_string_view& out) noexcept - { - try - { - size_t capacity{}; - RETURN_HR_IF(E_ABORT, !base::CheckAdd(in.length(), _partialsLen).AssignIfValid(&capacity)); - - _buffer.clear(); - - // If we were previously called with a huge buffer we have an equally large _buffer. - // We shouldn't just keep this huge buffer around, if no one needs it anymore. - if (_buffer.capacity() > 16 * 1024 && (_buffer.capacity() >> 1) > capacity) - { - _buffer.shrink_to_fit(); - } - - _buffer.reserve(capacity); - - // copy UTF-8 code units that were remaining from the previous call (if any) - if (_partialsLen != 0u) - { - _buffer.assign(_utfPartials.cbegin(), _utfPartials.cbegin() + _partialsLen); - _partialsLen = 0u; - } - - if (in.empty()) - { - out = _buffer; - if (_buffer.empty()) - { - return S_OK; - } - - return S_FALSE; // the partial is populated - } - - _buffer.append(in); - size_t remainingLength{ _buffer.length() }; - - auto backIter = _buffer.end(); - // If the last byte in the string was a byte belonging to a UTF-8 multi-byte character - if ((*(backIter - 1) & _Utf8BitMasks::MaskAsciiByte) > _Utf8BitMasks::IsAsciiByte) - { - // Check only up to 3 last bytes, if no Lead Byte was found then the byte before must be the Lead Byte and no partials are in the string - const size_t stopLen{ std::min(_buffer.length(), gsl::narrow_cast(3u)) }; - for (size_t sequenceLen{ 1u }; sequenceLen <= stopLen; ++sequenceLen) - { - --backIter; - // If Lead Byte found - if ((*backIter & _Utf8BitMasks::MaskContinuationByte) > _Utf8BitMasks::IsContinuationByte) - { - // If the Lead Byte indicates that the last bytes in the string is a partial UTF-8 code point then cache them: - // Use the bitmask at index `sequenceLen`. Compare the result with the operand having the same index. If they - // are not equal then the sequence has to be cached because it is a partial code point. Otherwise the - // sequence is a complete UTF-8 code point and the whole string is ready for the conversion into a UTF-16 string. - if ((*backIter & _cmpMasks.at(sequenceLen)) != _cmpOperands.at(sequenceLen)) - { - std::move(backIter, _buffer.end(), _utfPartials.begin()); - remainingLength -= sequenceLen; - _partialsLen = sequenceLen; - } - - break; - } - } - } - - // populate the part of the string that contains complete code points only - out = { _buffer.data(), remainingLength }; - - return S_OK; - } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } - } - - // Method Description: - // - Takes a UTF-16 string and populates it with *complete* UTF-16 codepoints. - // If it receives an incomplete codepoint, it will cache it until it can be completed. - // Arguments: - // - in - UTF-16 string_view potentially containing partial code points - // - out - on return, populated with complete codepoints at the string end - // Return Value: - // - S_OK - the resulting string doesn't end with a partial - // - S_FALSE - the resulting string contains the previously cached partials only - // - E_OUTOFMEMORY - the method failed to allocate memory for the resulting string - // - E_ABORT - the resulting string length would exceed the max_size and thus, the processing was aborted - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value, HRESULT>::type - operator()(const std::basic_string_view in, std::basic_string_view& out) noexcept - { - try - { - size_t remainingLength{ in.length() }; - size_t capacity{}; - - RETURN_HR_IF(E_ABORT, !base::CheckAdd(remainingLength, _partialsLen).AssignIfValid(&capacity)); - - _buffer.clear(); - _buffer.reserve(capacity); - - // copy UTF-8 code units that were remaining from the previous call (if any) - if (_partialsLen != 0u) - { - _buffer.push_back(_utfPartials.front()); - _partialsLen = 0u; - } - - if (in.empty()) - { - out = _buffer; - if (_buffer.empty()) - { - return S_OK; - } - - return S_FALSE; // the high surrogate is populated - } - - // cache the last value in the string if it is in the range of high surrogates - if (in.back() >= 0xD800u && in.back() <= 0xDBFFu) - { - _utfPartials.front() = in.back(); - --remainingLength; - _partialsLen = 1u; - } - else - { - _partialsLen = 0u; - } - - // populate the part of the string that contains complete code points only - _buffer.append(in, 0u, remainingLength); - out = _buffer; - - return S_OK; - } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } - } - - // Method Description: - // - Discard cached partials. - // Arguments: - // - none - // Return Value: - // - void - void reset() noexcept - { - _partialsLen = 0u; - } - - private: - enum _Utf8BitMasks : BYTE - { - IsAsciiByte = 0b0'0000000, // Any byte representing an ASCII character has the MSB set to 0 - MaskAsciiByte = 0b1'0000000, // Bit mask to be used in a bitwise AND operation to find out whether or not a byte match the IsAsciiByte pattern - IsContinuationByte = 0b10'000000, // Continuation bytes of any UTF-8 non-ASCII character have the MSB set to 1 and the adjacent bit set to 0 - MaskContinuationByte = 0b11'000000, // Bit mask to be used in a bitwise AND operation to find out whether or not a byte match the IsContinuationByte pattern - IsLeadByteTwoByteSequence = 0b110'00000, // A lead byte that indicates a UTF-8 non-ASCII character consisting of two bytes has the two highest bits set to 1 and the adjacent bit set to 0 - MaskLeadByteTwoByteSequence = 0b111'00000, // Bit mask to be used in a bitwise AND operation to find out whether or not a lead byte match the IsLeadByteTwoByteSequence pattern - IsLeadByteThreeByteSequence = 0b1110'0000, // A lead byte that indicates a UTF-8 non-ASCII character consisting of three bytes has the three highest bits set to 1 and the adjacent bit set to 0 - MaskLeadByteThreeByteSequence = 0b1111'0000, // Bit mask to be used in a bitwise AND operation to find out whether or not a lead byte match the IsLeadByteThreeByteSequence pattern - IsLeadByteFourByteSequence = 0b11110'000, // A lead byte that indicates a UTF-8 non-ASCII character consisting of four bytes has the four highest bits set to 1 and the adjacent bit set to 0 - MaskLeadByteFourByteSequence = 0b11111'000 // Bit mask to be used in a bitwise AND operation to find out whether or not a lead byte match the IsLeadByteFourByteSequence pattern - }; - - // array of bitmasks - constexpr static std::array _cmpMasks{ - 0, // unused - _Utf8BitMasks::MaskContinuationByte, - _Utf8BitMasks::MaskLeadByteTwoByteSequence, - _Utf8BitMasks::MaskLeadByteThreeByteSequence, - }; - - // array of values for the comparisons - constexpr static std::array _cmpOperands{ - 0, // unused - _Utf8BitMasks::IsAsciiByte, // intentionally conflicts with MaskContinuationByte - _Utf8BitMasks::IsLeadByteTwoByteSequence, - _Utf8BitMasks::IsLeadByteThreeByteSequence, - }; - - std::basic_string _buffer; // buffer to which the populated string_view refers - std::array _utfPartials; // buffer for code units of a partial code point that have to be cached - size_t _partialsLen{}; // number of cached code units }; - // make clear what incoming string type the state is for - typedef u8u16state u8state; - typedef u8u16state u16state; + // state structure for maintenance of UTF-16 partials + struct u16state + { + wchar_t partials[2]{}; + + constexpr void reset() noexcept + { + *this = {}; + } + }; // Routine Description: // - Takes a UTF-8 string and performs the conversion to UTF-16. NOTE: The function relies on getting complete UTF-8 characters at the string boundaries. @@ -269,62 +56,120 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // - S_OK - the conversion succeeded // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u8u16(const inT in, outT& out) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u8u16(const std::string_view& in, outT& out) noexcept { try { out.clear(); - - if (in.empty()) - { - return S_OK; - } + RETURN_HR_IF(S_OK, in.empty()); int lengthRequired{}; // The worst ratio of UTF-8 code units to UTF-16 code units is 1 to 1 if UTF-8 consists of ASCII only. RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(in.length()).AssignIfValid(&lengthRequired)); out.resize(in.length()); // avoid to call MultiByteToWideChar twice only to get the required size - const int lengthOut = MultiByteToWideChar(gsl::narrow_cast(CP_UTF8), 0ul, in.data(), lengthRequired, out.data(), lengthRequired); + const int lengthOut = MultiByteToWideChar(CP_UTF8, 0ul, in.data(), lengthRequired, out.data(), lengthRequired); out.resize(gsl::narrow_cast(lengthOut)); return lengthOut == 0 ? E_UNEXPECTED : S_OK; } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } + CATCH_RETURN(); } +#pragma warning(push) +#pragma warning(disable : 26429 26446 26459 26481 26482) // use not_null, subscript operator, use span, pointer arithmetic, dynamic array indexing // Routine Description: // - Takes a UTF-8 string, complements and/or caches partials, and performs the conversion to UTF-16. // Arguments: // - in - UTF-8 string to be converted // - out - reference to the resulting UTF-16 string - // - state - reference to a til::u8state class holding the status of the current partials handling + // - state - reference to a til::u8state holding the status of the current partials handling // Return Value: // - S_OK - the conversion succeeded // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u8u16(const inT in, outT& out, u8state& state) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u8u16(const std::string_view& in, outT& out, u8state& state) noexcept { - std::string_view sv{}; - RETURN_IF_FAILED(state(std::string_view{ in }, sv)); - return til::u8u16(sv, out); + try + { + out.clear(); + RETURN_HR_IF(S_OK, in.empty()); + + int capa16{}; + // The worst ratio of UTF-8 code units to UTF-16 code units is 1 to 1 if UTF-8 consists of ASCII only. + RETURN_HR_IF(E_ABORT, !base::CheckAdd(in.length(), state.have).AssignIfValid(&capa16)); + + out.resize(gsl::narrow_cast(capa16)); + auto len8{ gsl::narrow_cast(in.length()) }; + int len16{}; + auto cursor8{ in.data() }; + if (state.have) + { + const auto copyable{ std::min(state.want, len8) }; + std::move(cursor8, cursor8 + copyable, &state.partials[state.have]); + state.have += gsl::narrow_cast(copyable); + state.want -= gsl::narrow_cast(copyable); + if (state.want) // we still didn't get enough data to complete the code point, however this is not an error + { + out.clear(); + return S_OK; + } + + len16 = MultiByteToWideChar(CP_UTF8, 0UL, &state.partials[0], gsl::narrow_cast(state.have), out.data(), capa16); + RETURN_HR_IF(E_UNEXPECTED, !len16); + + capa16 -= len16; + len8 -= copyable; + cursor8 += copyable; + // state.want is already zero at this point + state.have = 0; + } + + if (len8) + { + auto backIter{ cursor8 + len8 - 1 }; + int sequenceLen{ 1 }; + + // skip UTF8 continuation bytes + while (backIter != cursor8 && (*backIter & 0b11'000000) == 0b10'000000) + { + --backIter; + ++sequenceLen; + } + + // credits go to Christopher Wellons for this algorithm to determine the length of a UTF-8 code point + // it is released into the Public Domain. https://github.com/skeeto/branchless-utf8 + static constexpr uint8_t lengths[]{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 3, 3, 4, 0 }; + const auto codePointLen{ lengths[gsl::narrow_cast(*backIter) >> 3] }; + + if (codePointLen > sequenceLen) + { + std::move(backIter, backIter + sequenceLen, &state.partials[0]); + len8 -= sequenceLen; + state.have = gsl::narrow_cast(sequenceLen); + state.want = gsl::narrow_cast(codePointLen - sequenceLen); + } + } + + if (len8) + { + const auto convLen{ MultiByteToWideChar(CP_UTF8, 0UL, cursor8, len8, out.data() + len16, capa16) }; + RETURN_HR_IF(E_UNEXPECTED, !convLen); + + len16 += convLen; + } + + out.resize(gsl::narrow_cast(len16)); + return S_OK; + } + CATCH_RETURN(); } +#pragma warning(pop) // Routine Description: // - Takes a UTF-16 string and performs the conversion to UTF-8. NOTE: The function relies on getting complete UTF-16 characters at the string boundaries. @@ -335,19 +180,15 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // - S_OK - the conversion succeeded // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u16u8(const inT in, outT& out) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u16u8(const std::wstring_view& in, outT& out) noexcept { try { out.clear(); - - if (in.empty()) - { - return S_OK; - } + RETURN_HR_IF(S_OK, in.empty()); int lengthIn{}; int lengthRequired{}; @@ -356,25 +197,16 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Thus, the worst ratio of UTF-16 code units to UTF-8 code units is 1 to 3. RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(in.length()).AssignIfValid(&lengthIn) || !base::CheckMul(lengthIn, 3).AssignIfValid(&lengthRequired)); out.resize(gsl::narrow_cast(lengthRequired)); // avoid to call WideCharToMultiByte twice only to get the required size - const int lengthOut = WideCharToMultiByte(gsl::narrow_cast(CP_UTF8), 0ul, in.data(), lengthIn, out.data(), lengthRequired, nullptr, nullptr); + const int lengthOut = WideCharToMultiByte(CP_UTF8, 0ul, in.data(), lengthIn, out.data(), lengthRequired, nullptr, nullptr); out.resize(gsl::narrow_cast(lengthOut)); return lengthOut == 0 ? E_UNEXPECTED : S_OK; } - catch (std::length_error&) - { - return E_ABORT; - } - catch (std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (...) - { - return E_UNEXPECTED; - } + CATCH_RETURN(); } +#pragma warning(push) +#pragma warning(disable : 26429 26446 26459 26481) // use not_null, subscript operator, use span, pointer arithmetic // Routine Description: // - Takes a UTF-16 string, complements and/or caches partials, and performs the conversion to UTF-8. // Arguments: @@ -385,15 +217,60 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // - S_OK - the conversion succeeded without any change of the represented code points // - E_OUTOFMEMORY - the function failed to allocate memory for the resulting string // - E_ABORT - the resulting string length would exceed the upper boundary of an int and thus, the conversion was aborted before the conversion has been completed - // - E_UNEXPECTED - an unexpected error occurred - template - [[nodiscard]] typename std::enable_if::value && std::is_same::value, HRESULT>::type - u16u8(const inT in, outT& out, u16state& state) noexcept + // - E_UNEXPECTED - the underlying conversion function failed + // - HRESULT value converted from a caught exception + template + [[nodiscard]] HRESULT u16u8(const std::wstring_view& in, outT& out, u16state& state) noexcept { - std::wstring_view sv{}; - RETURN_IF_FAILED(state(std::wstring_view{ in }, sv)); - return u16u8(sv, out); + try + { + out.clear(); + RETURN_HR_IF(S_OK, in.empty()); + + int len16{}; + int capa8{}; + // The worst ratio of UTF-16 code units to UTF-8 code units is 1 to 3. + RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(in.length()).AssignIfValid(&len16) || !base::CheckAdd(len16, gsl::narrow_cast(state.partials[0]) != 0).AssignIfValid(&capa8) || !base::CheckMul(capa8, 3).AssignIfValid(&capa8)); + + out.resize(gsl::narrow_cast(capa8)); + int len8{}; + auto cursor16{ in.data() }; + if (state.partials[0]) + { + state.partials[1] = *cursor16; + len8 = WideCharToMultiByte(CP_UTF8, 0UL, &state.partials[0], 2, out.data(), capa8, nullptr, nullptr); + RETURN_HR_IF(E_UNEXPECTED, !len8); + + state.reset(); + capa8 -= len8; + --len16; + ++cursor16; + } + + if (len16) + { + const auto back = *(cursor16 + len16 - 1); + if (back >= 0xD800 && back <= 0xDBFF) // cache the last value in the string if it is in the range of high surrogates + { + state.partials[0] = back; + --len16; + } + } + + if (len16) + { + const auto convLen{ WideCharToMultiByte(CP_UTF8, 0UL, cursor16, len16, out.data() + len8, capa8, nullptr, nullptr) }; + RETURN_HR_IF(E_UNEXPECTED, !convLen); + + len8 += convLen; + } + + out.resize(gsl::narrow_cast(len8)); + return S_OK; + } + CATCH_RETURN(); } +#pragma warning(pop) // Routine Description: // - Takes a UTF-8 string and performs the conversion to UTF-16. NOTE: The function relies on getting complete UTF-8 characters at the string boundaries. @@ -402,12 +279,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-16 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::wstring>::type - u8u16(const inT in) + inline std::wstring u8u16(const std::string_view& in) { std::wstring out{}; - THROW_IF_FAILED(u8u16(std::string_view{ in }, out)); + THROW_IF_FAILED(u8u16(in, out)); return out; } @@ -419,12 +294,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-16 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::wstring>::type - u8u16(const inT in, u8state& state) + inline std::wstring u8u16(const std::string_view& in, u8state& state) { std::wstring out{}; - THROW_IF_FAILED(u8u16(std::string_view{ in }, out, state)); + THROW_IF_FAILED(u8u16(in, out, state)); return out; } @@ -435,12 +308,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-8 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::string>::type - u16u8(const inT in) + inline std::string u16u8(const std::wstring_view& in) { std::string out{}; - THROW_IF_FAILED(u16u8(std::wstring_view{ in }, out)); + THROW_IF_FAILED(u16u8(in, out)); return out; } @@ -452,12 +323,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Return Value: // - the resulting UTF-8 string // - NOTE: Throws HRESULT errors that the non-throwing sibling returns - template - typename std::enable_if::value, std::string>::type - u16u8(const inT in, u16state& state) + inline std::string u16u8(const std::wstring_view& in, u16state& state) { std::string out{}; - THROW_IF_FAILED(u16u8(std::wstring_view{ in }, out, state)); + THROW_IF_FAILED(u16u8(in, out, state)); return out; } } diff --git a/src/interactivity/base/EventSynthesis.cpp b/src/interactivity/base/EventSynthesis.cpp index 9d1c217d9..e73c06727 100644 --- a/src/interactivity/base/EventSynthesis.cpp +++ b/src/interactivity/base/EventSynthesis.cpp @@ -181,7 +181,6 @@ std::deque> Microsoft::Console::Interactivity::Synthes UNICODE_NULL, LEFT_ALT_PRESSED)); - const int radix = 10; std::wstring wstr{ wch }; const auto convertedChars = ConvertToA(codepage, wstr); if (convertedChars.size() == 1) diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index cf690ecd3..dcd3f923e 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -196,6 +196,7 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid [[nodiscard]] HRESULT BgfxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, bool const /*isSettingDefaultBrushes*/) noexcept { _currentLegacyColorAttribute = textAttributes.GetLegacyAttributes(); diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 4fddcfb0b..2b4e787a3 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -60,6 +60,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, bool const isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/interactivity/win32/ut_interactivity_win32/GeneratedUiaTextRangeMovementTests.g.cpp b/src/interactivity/win32/ut_interactivity_win32/GeneratedUiaTextRangeMovementTests.g.cpp new file mode 100644 index 000000000..b8944f10c --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/GeneratedUiaTextRangeMovementTests.g.cpp @@ -0,0 +1,3171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// DO NOT MODIFY THESE TESTS DIRECTLY +// These were generated by tools\TestTableWriter\GenerateTests.ps1 +// Read tools\TestTableWriter\README.md for more details +// Define a few helpful variables +constexpr til::rectangle bufferSize{ 0, 0, 80, 300 }; +constexpr short midX{ 40 }; +constexpr short midY{ 150 }; +constexpr short midPopulatedY{ 75 }; +constexpr til::point origin{ 0, 0 }; +constexpr til::point midTop{ midX, 0 }; +constexpr til::point midHistory{ midX, midPopulatedY }; +constexpr til::point midDocEnd{ midX, midY }; +constexpr til::point lastCharPos{ 79, midY }; +constexpr til::point docEnd{ 0, midY + 1 }; +constexpr til::point midEmptySpace{ midX, midY + midPopulatedY }; +constexpr til::point bufferEnd{ 79, 299 }; +constexpr til::point endExclusive{ 0, 300 }; + +constexpr til::point bufferEndLeft{ bufferSize.left(), bufferEnd.y() }; +constexpr auto bufferEndM1C{ point_offset_by_char(bufferEnd, bufferSize, -1) }; +constexpr auto bufferEndM1L{ point_offset_by_line(bufferEnd, bufferSize, -1) }; +constexpr auto bufferEndM4C{ point_offset_by_char(bufferEnd, bufferSize, -4) }; +constexpr auto bufferEndM4L{ point_offset_by_line(bufferEnd, bufferSize, -4) }; +constexpr auto bufferEndM5C{ point_offset_by_char(bufferEnd, bufferSize, -5) }; +constexpr auto bufferEndM5L{ point_offset_by_line(bufferEnd, bufferSize, -5) }; +constexpr til::point docEndLeft{ bufferSize.left(), docEnd.y() }; +constexpr auto docEndM1C{ point_offset_by_char(docEnd, bufferSize, -1) }; +constexpr auto docEndM4L{ point_offset_by_line(docEnd, bufferSize, -4) }; +constexpr auto docEndM5C{ point_offset_by_char(docEnd, bufferSize, -5) }; +constexpr auto docEndP1C{ point_offset_by_char(docEnd, bufferSize, 1) }; + +constexpr til::point lastCharPosLeft{ bufferSize.left(), lastCharPos.y() }; +constexpr auto lastCharPosM1C{ point_offset_by_char(lastCharPos, bufferSize, -1) }; +constexpr auto lastCharPosM1L{ point_offset_by_line(lastCharPos, bufferSize, -1) }; +constexpr auto lastCharPosM4C{ point_offset_by_char(lastCharPos, bufferSize, -4) }; +constexpr auto lastCharPosM4L{ point_offset_by_line(lastCharPos, bufferSize, -4) }; +constexpr auto lastCharPosM5C{ point_offset_by_char(lastCharPos, bufferSize, -5) }; +constexpr auto lastCharPosM5L{ point_offset_by_line(lastCharPos, bufferSize, -5) }; +constexpr auto lastCharPosP1C{ point_offset_by_char(lastCharPos, bufferSize, 1) }; + +constexpr til::point midDocEndLeft{ bufferSize.left(), midDocEnd.y() }; +constexpr auto midDocEndM1C{ point_offset_by_char(midDocEnd, bufferSize, -1) }; +constexpr auto midDocEndM1L{ point_offset_by_line(midDocEnd, bufferSize, -1) }; +constexpr auto midDocEndM4C{ point_offset_by_char(midDocEnd, bufferSize, -4) }; +constexpr auto midDocEndM4L{ point_offset_by_line(midDocEnd, bufferSize, -4) }; +constexpr auto midDocEndM5C{ point_offset_by_char(midDocEnd, bufferSize, -5) }; +constexpr auto midDocEndM5L{ point_offset_by_line(midDocEnd, bufferSize, -5) }; +constexpr auto midDocEndP1C{ point_offset_by_char(midDocEnd, bufferSize, 1) }; +constexpr auto midDocEndP2C{ point_offset_by_char(midDocEnd, bufferSize, 2) }; +constexpr auto midDocEndP5C{ point_offset_by_char(midDocEnd, bufferSize, 5) }; +constexpr auto midDocEndP6C{ point_offset_by_char(midDocEnd, bufferSize, 6) }; + +constexpr auto midEmptySpaceP1C{ point_offset_by_char(midEmptySpace, bufferSize, 1) }; +constexpr til::point midHistoryLeft{ bufferSize.left(), midHistory.y() }; +constexpr auto midHistoryM1C{ point_offset_by_char(midHistory, bufferSize, -1) }; +constexpr auto midHistoryM1L{ point_offset_by_line(midHistory, bufferSize, -1) }; +constexpr auto midHistoryM4C{ point_offset_by_char(midHistory, bufferSize, -4) }; +constexpr auto midHistoryM4L{ point_offset_by_line(midHistory, bufferSize, -4) }; +constexpr auto midHistoryM5C{ point_offset_by_char(midHistory, bufferSize, -5) }; +constexpr auto midHistoryM5L{ point_offset_by_line(midHistory, bufferSize, -5) }; +constexpr auto midHistoryP1C{ point_offset_by_char(midHistory, bufferSize, 1) }; +constexpr auto midHistoryP1L{ point_offset_by_line(midHistory, bufferSize, 1) }; +constexpr auto midHistoryP2C{ point_offset_by_char(midHistory, bufferSize, 2) }; +constexpr auto midHistoryP2L{ point_offset_by_line(midHistory, bufferSize, 2) }; +constexpr auto midHistoryP5C{ point_offset_by_char(midHistory, bufferSize, 5) }; +constexpr auto midHistoryP5L{ point_offset_by_line(midHistory, bufferSize, 5) }; +constexpr auto midHistoryP6C{ point_offset_by_char(midHistory, bufferSize, 6) }; +constexpr auto midHistoryP6L{ point_offset_by_line(midHistory, bufferSize, 6) }; +constexpr auto midTopM1C{ point_offset_by_char(midTop, bufferSize, -1) }; +constexpr auto midTopM4C{ point_offset_by_char(midTop, bufferSize, -4) }; +constexpr auto midTopM5C{ point_offset_by_char(midTop, bufferSize, -5) }; +constexpr auto midTopP1C{ point_offset_by_char(midTop, bufferSize, 1) }; +constexpr auto midTopP1L{ point_offset_by_line(midTop, bufferSize, 1) }; +constexpr auto midTopP2C{ point_offset_by_char(midTop, bufferSize, 2) }; +constexpr auto midTopP5C{ point_offset_by_char(midTop, bufferSize, 5) }; +constexpr auto midTopP5L{ point_offset_by_line(midTop, bufferSize, 5) }; +constexpr auto midTopP6C{ point_offset_by_char(midTop, bufferSize, 6) }; +constexpr auto originP1C{ point_offset_by_char(origin, bufferSize, 1) }; +constexpr auto originP1L{ point_offset_by_line(origin, bufferSize, 1) }; +constexpr auto originP2C{ point_offset_by_char(origin, bufferSize, 2) }; +constexpr auto originP2L{ point_offset_by_line(origin, bufferSize, 2) }; +constexpr auto originP5C{ point_offset_by_char(origin, bufferSize, 5) }; +constexpr auto originP5L{ point_offset_by_line(origin, bufferSize, 5) }; +constexpr auto originP6C{ point_offset_by_char(origin, bufferSize, 6) }; +constexpr auto originP6L{ point_offset_by_line(origin, bufferSize, 6) }; +struct GeneratedMovementTestInput +{ + TextUnit unit; + int moveAmount; + til::point start; + til::point end; +}; +struct GeneratedMovementTestExpected +{ + int moveAmount; + til::point start; + til::point end; +}; +struct GeneratedMovementTest +{ + std::wstring_view name; + GeneratedMovementTestInput input; + GeneratedMovementTestExpected expected; + bool skip; +}; + +static constexpr std::array s_movementTests{ + GeneratedMovementTest{ + L"Move degenerate range at position 1 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + origin, + origin }, + GeneratedMovementTestExpected{ + 1, + originP1C, + originP1C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + origin, + origin }, + GeneratedMovementTestExpected{ + 5, + originP5C, + originP5C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 1, + originP1C, + originP2C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 5, + originP5C, + originP6C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midTop, + midTop }, + GeneratedMovementTestExpected{ + -5, + midTopM5C, + midTopM5C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midTop, + midTop }, + GeneratedMovementTestExpected{ + -1, + midTopM1C, + midTopM1C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 0, + midTop, + midTop }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 1, + midTopP1C, + midTopP1C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 5, + midTopP5C, + midTopP5C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + -5, + midTopM5C, + midTopM4C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + -1, + midTopM1C, + midTop }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + midTop, + midTopP1C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 1, + midTopP1C, + midTopP2C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 5, + midTopP5C, + midTopP6C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + -5, + midHistoryM5C, + midHistoryM5C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + -1, + midHistoryM1C, + midHistoryM1C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 0, + midHistory, + midHistory }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 1, + midHistoryP1C, + midHistoryP1C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 5, + midHistoryP5C, + midHistoryP5C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + -5, + midHistoryM5C, + midHistoryM4C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + -1, + midHistoryM1C, + midHistory }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 0, + midHistory, + midHistoryP1C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 1, + midHistoryP1C, + midHistoryP2C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 5, + midHistoryP5C, + midHistoryP6C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + origin, + origin }, + GeneratedMovementTestExpected{ + 1, + originP1L, + originP1L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + origin, + origin }, + GeneratedMovementTestExpected{ + 5, + originP5L, + originP5L }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 1, + originP1L, + originP2L }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 5, + originP5L, + originP6L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midTop, + midTop }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midTop, + midTop }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 0, + midTop, + midTop }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 1, + midTopP1L, + midTopP1L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 5, + midTopP5L, + midTopP5L }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + originP1L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 1, + originP1L, + originP2L }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 5, + originP5L, + originP6L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + -5, + midHistoryM4L, + midHistoryM4L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + -1, + midHistoryLeft, + midHistoryLeft }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 0, + midHistory, + midHistory }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 1, + midHistoryP1L, + midHistoryP1L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 5, + midHistoryP5L, + midHistoryP5L }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + -5, + midHistoryM5L, + midHistoryM4L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + -1, + midHistoryM1L, + midHistoryLeft }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 0, + midHistoryLeft, + midHistoryP1L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 1, + midHistoryP1L, + midHistoryP2L }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 5, + midHistoryP5L, + midHistoryP6L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + origin, + origin }, + GeneratedMovementTestExpected{ + 0, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + origin, + origin }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 1 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + origin, + origin }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 1 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + origin, + originP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midTop, + midTop }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midTop, + midTop }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 0, + midTop, + midTop }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 1, + bufferEnd, + bufferEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 2 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midTop, + midTop }, + GeneratedMovementTestExpected{ + 1, + bufferEnd, + bufferEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + bufferEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + bufferEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + bufferEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + bufferEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 2 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midTop, + midTopP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + bufferEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 0, + midHistory, + midHistory }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 3 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midHistory, + midHistory }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 3 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midHistory, + midHistoryP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + -5, + bufferEndM5C, + bufferEndM5C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + -1, + bufferEndM1C, + bufferEndM1C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 0, + bufferEnd, + bufferEnd }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + -5, + bufferEndM5C, + bufferEndM4C }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + -1, + bufferEndM1C, + bufferEnd }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + bufferEnd, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + bufferEnd, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + bufferEnd, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + -5, + bufferEndM4L, + bufferEndM4L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + -1, + bufferEndLeft, + bufferEndLeft }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 0, + bufferEnd, + bufferEnd }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + -5, + bufferEndM5L, + bufferEndM4L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + -1, + bufferEndM1L, + bufferEndLeft }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + bufferEndLeft, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + bufferEndLeft, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + bufferEndLeft, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 0, + bufferEnd, + bufferEnd }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 8 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + bufferEnd, + bufferEnd }, + GeneratedMovementTestExpected{ + 1, + endExclusive, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 8 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + bufferEnd, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + origin, + endExclusive }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + -5, + bufferEndM4C, + bufferEndM4C }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + -1, + bufferEnd, + bufferEnd }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + -5, + bufferEndM4L, + bufferEndM4L }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + -1, + bufferEndLeft, + bufferEndLeft }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 9 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + endExclusive, + endExclusive }, + GeneratedMovementTestExpected{ + 0, + endExclusive, + endExclusive }, + false }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + -5, + midDocEndM5C, + midDocEndM5C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + -1, + midDocEndM1C, + midDocEndM1C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 0, + midDocEnd, + midDocEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 1, + midDocEndP1C, + midDocEndP1C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 5, + midDocEndP5C, + midDocEndP5C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + -5, + midDocEndM5C, + midDocEndM4C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + -1, + midDocEndM1C, + midDocEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + midDocEnd, + midDocEndP1C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 1, + midDocEndP1C, + midDocEndP2C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 5, + midDocEndP5C, + midDocEndP6C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + -5, + midDocEndM4L, + midDocEndM4L }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + -1, + midDocEndLeft, + midDocEndLeft }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 0, + midDocEnd, + midDocEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + -5, + midDocEndM5L, + midDocEndM4L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + -1, + midDocEndM1L, + midDocEndLeft }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + midDocEndLeft, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + midDocEndLeft, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + midDocEndLeft, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 0, + midDocEnd, + midDocEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 4 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midDocEnd, + midDocEnd }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 4 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midDocEnd, + midDocEndP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + -5, + lastCharPosM5C, + lastCharPosM5C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + -1, + lastCharPosM1C, + lastCharPosM1C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 0, + lastCharPos, + lastCharPos }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + -5, + lastCharPosM5C, + lastCharPosM4C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + -1, + lastCharPosM1C, + lastCharPos }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + lastCharPos, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + lastCharPos, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + lastCharPos, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + -5, + lastCharPosM4L, + lastCharPosM4L }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + -1, + lastCharPosLeft, + lastCharPosLeft }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 0, + lastCharPos, + lastCharPos }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + -5, + lastCharPosM5L, + lastCharPosM4L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + -1, + lastCharPosM1L, + lastCharPosLeft }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + lastCharPosLeft, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + lastCharPosLeft, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + lastCharPosLeft, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 0, + lastCharPos, + lastCharPos }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 5 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + lastCharPos, + lastCharPos }, + GeneratedMovementTestExpected{ + 1, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 5 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + lastCharPos, + lastCharPosP1C }, + GeneratedMovementTestExpected{ + 0, + origin, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + -5, + docEndM5C, + docEndM5C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + -1, + docEndM1C, + docEndM1C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + -5, + docEndM5C, + docEndM5C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + -1, + docEndM1C, + docEndM1C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + -5, + docEndM4L, + docEndM4L }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + -1, + docEndLeft, + docEndLeft }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + -5, + docEndM4L, + docEndM4L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + -1, + docEndLeft, + docEndLeft }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 6 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + docEnd, + docEnd }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 6 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + docEnd, + docEndP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + -5, + docEndM5C, + docEndM5C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + -1, + docEndM1C, + docEndM1C }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 -5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -5, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + -5, + docEndM5C, + docEndM5C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 -1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + -1, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + -1, + docEndM1C, + docEndM1C }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 0 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 0, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 1 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 1, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 5 times by Character", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Character, + 5, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + -5, + docEndM4L, + docEndM4L }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + -1, + docEndLeft, + docEndLeft }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 -5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -5, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + -5, + docEndM4L, + docEndM4L }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 -1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + -1, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + -1, + docEndLeft, + docEndLeft }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 0 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 0, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 1 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 1, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 5 times by Line", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Line, + 5, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move degenerate range at position 7 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midEmptySpace, + midEmptySpace }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 -5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -5, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 -1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + -1, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + -1, + origin, + origin }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 0 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 0, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 1 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 1, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, + GeneratedMovementTest{ + L"Move non-degenerate range at position 7 5 times by Document", + GeneratedMovementTestInput{ + TextUnit::TextUnit_Document, + 5, + midEmptySpace, + midEmptySpaceP1C }, + GeneratedMovementTestExpected{ + 0, + docEnd, + docEnd }, + true }, +}; diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp index 5457b9853..26da4fe88 100644 --- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -11,6 +11,8 @@ #include "../../../buffer/out/textBuffer.hpp" #include "../types/UiaTracing.h" +#include + using namespace WEX::Common; using namespace WEX::Logging; using namespace WEX::TestExecution; @@ -19,13 +21,193 @@ using namespace Microsoft::WRL; using namespace Microsoft::Console::Interactivity::Win32; +static constexpr til::point point_offset_by_char(const til::point start, const til::rectangle bounds, ptrdiff_t amt) +{ + ptrdiff_t pos_x = start.x(); + ptrdiff_t pos_y = start.y(); + while (amt != 0) + { + if (amt > 0) + { + if (pos_x == bounds.left() && pos_y == bounds.bottom()) + { + // end exclusive --> can't move any more + break; + } + else if (pos_x == bounds.right() - 1) + { + // right boundary --> wrap + pos_x = bounds.left(); + ++pos_y; + } + else + { + // standard move + ++pos_x; + } + --amt; + } + else + { + if (pos_x == bounds.left() && pos_y == bounds.top()) + { + // origin --> can't move any more + break; + } + else if (pos_x == bounds.left()) + { + // left boundary --> wrap + pos_x = bounds.right() - 1; + --pos_y; + } + else + { + // standard move + --pos_x; + } + ++amt; + } + } + return { pos_x, pos_y }; +} + +static constexpr til::point point_offset_by_line(const til::point start, const til::rectangle bounds, ptrdiff_t amt) +{ + // X = left boundary for UIA + ptrdiff_t pos_x = bounds.left(); + ptrdiff_t pos_y = start.y(); + while (amt != 0) + { + if (amt > 0) + { + if (pos_y == bounds.bottom() + 1) + { + break; + } + else + { + ++pos_y; + } + --amt; + } + else + { + if (pos_y == bounds.top()) + { + break; + } + else + { + --pos_y; + } + ++amt; + } + } + return { pos_x, pos_y }; +} + +// IMPORTANT: reference this _after_ defining point_offset_by_XXX. We need it for some definitions +#include "GeneratedUiaTextRangeMovementTests.g.cpp" + +namespace +{ +#pragma region TAEF hookup for the test case array above + struct ArrayIndexTaefAdapterRow : public Microsoft::WRL::RuntimeClass, IDataRow> + { + HRESULT RuntimeClassInitialize(const size_t index) + { + _index = index; + return S_OK; + } + + STDMETHODIMP GetTestData(BSTR /*pszName*/, SAFEARRAY** ppData) override + { + const auto indexString{ wil::str_printf(L"%zu", _index) }; + auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; + LONG index{ 0 }; + auto indexBstr{ wil::make_bstr(indexString.c_str()) }; + (void)SafeArrayPutElement(safeArray, &index, indexBstr.release()); + *ppData = safeArray; + return S_OK; + } + + STDMETHODIMP GetMetadataNames(SAFEARRAY** ppMetadataNames) override + { + *ppMetadataNames = nullptr; + return S_FALSE; + } + + STDMETHODIMP GetMetadata(BSTR /*pszName*/, SAFEARRAY** ppData) override + { + *ppData = nullptr; + return S_FALSE; + } + + STDMETHODIMP GetName(BSTR* ppszRowName) override + { + *ppszRowName = wil::make_bstr(s_movementTests[_index].name.data()).release(); + return S_OK; + } + + private: + size_t _index; + }; + + struct ArrayIndexTaefAdapterSource : public Microsoft::WRL::RuntimeClass, IDataSource> + { + STDMETHODIMP Advance(IDataRow** ppDataRow) override + { + if (_index < s_movementTests.size()) + { + Microsoft::WRL::MakeAndInitialize(ppDataRow, _index++); + } + else + { + *ppDataRow = nullptr; + } + return S_OK; + } + + STDMETHODIMP Reset() override + { + _index = 0; + return S_OK; + } + + STDMETHODIMP GetTestDataNames(SAFEARRAY** names) override + { + auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; + LONG index{ 0 }; + auto dataNameBstr{ wil::make_bstr(L"index") }; + (void)SafeArrayPutElement(safeArray, &index, dataNameBstr.release()); + *names = safeArray; + return S_OK; + } + + STDMETHODIMP GetTestDataType(BSTR /*name*/, BSTR* type) override + { + *type = nullptr; + return S_OK; + } + + private: + size_t _index{ 0 }; + }; +#pragma endregion +} + +extern "C" HRESULT __declspec(dllexport) __cdecl GeneratedMovementTestDataSource(IDataSource** ppDataSource, void*) +{ + auto source{ Microsoft::WRL::Make() }; + return source.CopyTo(ppDataSource); +} + // UiaTextRange takes an object that implements // IRawElementProviderSimple as a constructor argument. Making a real // one would involve setting up the window which we don't want to do // for unit tests so instead we'll use this one. We don't care about // it not doing anything for its implementation because it is not used // during the unit tests below. - class DummyElementProvider final : public ScreenInfoUiaProviderBase { public: @@ -197,9 +379,6 @@ class UiaTextRangeTests TEST_METHOD(DegenerateRangesDetected) { - const auto bufferSize = _pTextBuffer->GetSize(); - const auto origin = bufferSize.Origin(); - // make a degenerate range and verify that it reports degenerate Microsoft::WRL::ComPtr degenerate; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(°enerate, @@ -211,7 +390,7 @@ class UiaTextRangeTests VERIFY_ARE_EQUAL(degenerate->_start, degenerate->_end); // make a non-degenerate range and verify that it reports as such - const COORD end = { origin.X + 1, origin.Y }; + const auto end{ point_offset_by_char(origin, bufferSize, 1) }; Microsoft::WRL::ComPtr notDegenerate; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(¬Degenerate, _pUiaData, @@ -224,9 +403,6 @@ class UiaTextRangeTests TEST_METHOD(CompareRange) { - const auto bufferSize = _pTextBuffer->GetSize(); - const auto origin = bufferSize.Origin(); - Microsoft::WRL::ComPtr utr1; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr1, _pUiaData, @@ -244,7 +420,7 @@ class UiaTextRangeTests VERIFY_IS_TRUE(comparison); // utr2 redefined to have different end from utr1 - const COORD end = { origin.X + 2, origin.Y }; + const auto end{ point_offset_by_char(origin, bufferSize, 2) }; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr2, _pUiaData, &_dummyProvider, @@ -258,9 +434,6 @@ class UiaTextRangeTests TEST_METHOD(CompareEndpoints) { - const auto bufferSize = _pTextBuffer->GetSize(); - const auto origin = bufferSize.Origin(); - Microsoft::WRL::ComPtr utr1; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr1, _pUiaData, @@ -283,7 +456,7 @@ class UiaTextRangeTests VERIFY_IS_TRUE(comparison == 0); // utr2 redefined to have different end from utr1 - const COORD end = { origin.X + 2, origin.Y }; + const auto end{ point_offset_by_char(origin, bufferSize, 2) }; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr2, _pUiaData, &_dummyProvider, @@ -465,8 +638,6 @@ class UiaTextRangeTests start, end)); - const auto bufferSize = _pTextBuffer->GetSize(); - const auto origin = bufferSize.Origin(); Microsoft::WRL::ComPtr target; auto resetTargetUTR = [&]() { @@ -1137,9 +1308,7 @@ class UiaTextRangeTests // at the end exclusive, the UTR should refuse to move past // the end. - const auto bufferSize{ _pTextBuffer->GetSize() }; - const til::point endInclusive{ bufferSize.RightInclusive(), bufferSize.BottomInclusive() }; - const auto endExclusive{ bufferSize.EndExclusive() }; + const til::point endInclusive{ bufferEnd }; // Iterate over each TextUnit. If the we don't support // the given TextUnit, we're supposed to fallback @@ -1153,7 +1322,7 @@ class UiaTextRangeTests THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, endInclusive, endExclusive)); THROW_IF_FAILED(utr->ExpandToEnclosingUnit(static_cast(unit))); - VERIFY_ARE_EQUAL(endExclusive, utr->_end); + VERIFY_ARE_EQUAL(endExclusive, til::point{ utr->_end }); } } @@ -1162,12 +1331,9 @@ class UiaTextRangeTests // GH#7663: When attempting to move from end exclusive, // the UTR should refuse to move past the end. - const auto bufferSize{ _pTextBuffer->GetSize() }; - const COORD origin{ bufferSize.Origin() }; - const COORD lastLineStart{ bufferSize.Left(), bufferSize.BottomInclusive() }; - const COORD secondToLastCharacterPos{ bufferSize.RightInclusive() - 1, bufferSize.BottomInclusive() }; - const COORD endInclusive{ bufferSize.RightInclusive(), bufferSize.BottomInclusive() }; - const COORD endExclusive{ bufferSize.EndExclusive() }; + const auto lastLineStart{ bufferEndLeft }; + const auto secondToLastCharacterPos{ point_offset_by_char(bufferEnd, bufferSize, -1) }; + const auto endInclusive{ bufferEnd }; // Iterate over each TextUnit. If we don't support // the given TextUnit, we're supposed to fallback @@ -1198,11 +1364,11 @@ class UiaTextRangeTests } THROW_IF_FAILED(utr->Move(textUnit, 1, &moveAmt)); - VERIFY_ARE_EQUAL(endExclusive, utr->_end); + VERIFY_ARE_EQUAL(endExclusive, til::point{ utr->_end }); VERIFY_ARE_EQUAL(0, moveAmt); // write "temp" at (2,2) - const COORD writeTarget{ 2, 2 }; + const til::point writeTarget{ 2, 2 }; _pTextBuffer->Write({ L"temp" }, writeTarget); // Verify expansion works properly @@ -1210,23 +1376,23 @@ class UiaTextRangeTests THROW_IF_FAILED(utr->ExpandToEnclosingUnit(textUnit)); if (textUnit <= TextUnit::TextUnit_Character) { - VERIFY_ARE_EQUAL(endInclusive, utr->_start); - VERIFY_ARE_EQUAL(endExclusive, utr->_end); + VERIFY_ARE_EQUAL(endInclusive, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(endExclusive, til::point{ utr->_end }); } else if (textUnit <= TextUnit::TextUnit_Word) { - VERIFY_ARE_EQUAL(writeTarget, utr->_start); - VERIFY_ARE_EQUAL(endExclusive, utr->_end); + VERIFY_ARE_EQUAL(writeTarget, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(endExclusive, til::point{ utr->_end }); } else if (textUnit <= TextUnit::TextUnit_Line) { - VERIFY_ARE_EQUAL(lastLineStart, utr->_start); - VERIFY_ARE_EQUAL(endExclusive, utr->_end); + VERIFY_ARE_EQUAL(lastLineStart, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(endExclusive, til::point{ utr->_end }); } else // textUnit <= TextUnit::TextUnit_Document: { - VERIFY_ARE_EQUAL(origin, utr->_start); - VERIFY_ARE_EQUAL(endExclusive, utr->_end); + VERIFY_ARE_EQUAL(origin, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(endExclusive, til::point{ utr->_end }); } // reset the UTR @@ -1250,23 +1416,23 @@ class UiaTextRangeTests // Special case: _end will always be endInclusive, because... // - degenerate --> it moves with _start to stay degenerate // - !degenerate --> it excludes the last char, to select the second to last char - VERIFY_ARE_EQUAL(degenerate ? endInclusive : secondToLastCharacterPos, utr->_start); - VERIFY_ARE_EQUAL(endInclusive, utr->_end); + VERIFY_ARE_EQUAL(degenerate ? endInclusive : secondToLastCharacterPos, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(endInclusive, til::point{ utr->_end }); } else if (textUnit <= TextUnit::TextUnit_Word) { - VERIFY_ARE_EQUAL(origin, utr->_start); - VERIFY_ARE_EQUAL(degenerate ? origin : writeTarget, utr->_end); + VERIFY_ARE_EQUAL(origin, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(degenerate ? origin : writeTarget, til::point{ utr->_end }); } else if (textUnit <= TextUnit::TextUnit_Line) { - VERIFY_ARE_EQUAL(lastLineStart, utr->_start); - VERIFY_ARE_EQUAL(degenerate ? lastLineStart : endExclusive, utr->_end); + VERIFY_ARE_EQUAL(lastLineStart, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(degenerate ? lastLineStart : endExclusive, til::point{ utr->_end }); } else // textUnit <= TextUnit::TextUnit_Document: { - VERIFY_ARE_EQUAL(origin, utr->_start); - VERIFY_ARE_EQUAL(degenerate ? origin : endExclusive, utr->_end); + VERIFY_ARE_EQUAL(origin, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(degenerate ? origin : endExclusive, til::point{ utr->_end }); } } @@ -1274,9 +1440,7 @@ class UiaTextRangeTests { // See GH#7742 for more details. - const auto bufferSize{ _pTextBuffer->GetSize() }; - const COORD origin{ bufferSize.Origin() }; - const COORD originExclusive{ origin.X, origin.Y + 1 }; + const auto originExclusive{ point_offset_by_char(origin, bufferSize, 1) }; _pTextBuffer->Write({ L"My name is Carlos" }, origin); @@ -1314,21 +1478,20 @@ class UiaTextRangeTests TEST_METHOD(ScrollIntoView) { - const auto bufferSize{ _pTextBuffer->GetSize() }; const auto viewportSize{ _pUiaData->GetViewport() }; const std::vector testData{ - { L"Origin", bufferSize.Top() }, - { L"ViewportHeight From Top - 1", bufferSize.Top() + viewportSize.Height() - 1 }, - { L"ViewportHeight From Top", bufferSize.Top() + viewportSize.Height() }, - { L"ViewportHeight From Top + 1", bufferSize.Top() + viewportSize.Height() + 1 }, - { L"ViewportHeight From Bottom - 1", bufferSize.BottomInclusive() - viewportSize.Height() - 1 }, - { L"ViewportHeight From Bottom", bufferSize.BottomInclusive() - viewportSize.Height() }, - { L"ViewportHeight From Bottom + 1", bufferSize.BottomInclusive() - viewportSize.Height() + 1 }, + { L"Origin", gsl::narrow(bufferSize.top()) }, + { L"ViewportHeight From Top - 1", base::ClampedNumeric(bufferSize.top()) + viewportSize.Height() - 1 }, + { L"ViewportHeight From Top", base::ClampedNumeric(bufferSize.top()) + viewportSize.Height() }, + { L"ViewportHeight From Top + 1", base::ClampedNumeric(bufferSize.top()) + viewportSize.Height() + 1 }, + { L"ViewportHeight From Bottom - 1", base::ClampedNumeric(bufferSize.bottom()) - viewportSize.Height() - 2 }, + { L"ViewportHeight From Bottom", base::ClampedNumeric(bufferSize.bottom()) - viewportSize.Height() - 1 }, + { L"ViewportHeight From Bottom + 1", base::ClampedNumeric(bufferSize.bottom()) - viewportSize.Height() + 1 }, // GH#7839: ExclusiveEnd is a non-existent space, // so scrolling to it when !alignToTop used to crash - { L"Exclusive End", bufferSize.BottomExclusive() } + { L"Exclusive End", gsl::narrow(bufferSize.bottom()) } }; BEGIN_TEST_METHOD_PROPERTIES() @@ -1342,7 +1505,7 @@ class UiaTextRangeTests for (const auto test : testData) { Log::Comment(test.comment.c_str()); - const til::point pos{ bufferSize.Left(), test.yPos }; + const til::point pos{ bufferSize.left(), test.yPos }; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, pos, pos)); VERIFY_SUCCEEDED(utr->ScrollIntoView(alignToTop)); } @@ -1663,4 +1826,140 @@ class UiaTextRangeTests UiaTextRange* cloneUtr2 = static_cast(clone2.Get()); VERIFY_IS_TRUE(cloneUtr2->_blockRange); } + + TEST_METHOD(Movement) + { + // Helpful variables + const auto firstChar{ point_offset_by_char(origin, bufferSize, 1) }; + const auto secondChar{ point_offset_by_char(origin, bufferSize, 2) }; + const auto fifthChar{ point_offset_by_char(origin, bufferSize, 5) }; + const auto sixthChar{ point_offset_by_char(origin, bufferSize, 6) }; + const til::point documentEnd{ bufferSize.left(), (bufferSize.height() / 2) + 1 }; + + // Populate buffer + // Split the line into 5 segments alternating between "X" and whitespace + // _________________ + // |XXX XXX XXX| + // |XXX XXX XXX| + // |XXX XXX XXX| + // |XXX XXX XXX| + // |_______________| + { + short i = 0; + auto iter{ _pTextBuffer->GetCellDataAt(origin) }; + const auto segment{ bufferSize.width() / 5 }; + while (iter.Pos() != documentEnd) + { + bool fill{ true }; + if (i % segment == 0) + { + fill = !fill; + } + + if (fill) + { + _pTextBuffer->Write({ L"X" }, iter.Pos()); + } + + ++i; + ++iter; + } + } + + // Define tests + struct TestInput + { + TextUnit unit; + int moveAmt; + til::point start; + til::point end; + }; + + struct TestOutput + { + int moveAmt; + til::point start; + til::point end; + }; + + struct MyTest + { + std::wstring name; + TestInput input; + TestOutput output; + }; + + const std::vector tests{ + MyTest{ L"Degenerate at origin", TestInput{ TextUnit_Character, -5, origin, origin }, TestOutput{ 0, origin, origin } } + }; + } + + TEST_METHOD(GeneratedMovementTests) + { + // Populate the buffer with... + // - 9 segments of alternating text + // - up to half of the buffer (vertically) + // It'll look something like this + // +---------------------------+ + // |XXX XXX XXX XXX XXX| + // |XXX XXX XXX XXX XXX| + // |XXX XXX XXX XXX XXX| + // |XXX XXX XXX XXX XXX| + // |XXX XXX XXX XXX XXX| + // | | + // | | + // | | + // | | + // | | + // +---------------------------+ + { + short i = 0; + auto iter{ _pTextBuffer->GetCellDataAt(bufferSize.origin()) }; + const auto segment{ bufferSize.width() / 9 }; + while (iter.Pos() != docEnd) + { + bool fill{ true }; + if (i % segment == 0) + { + fill = !fill; + } + + if (fill) + { + _pTextBuffer->Write({ L"X" }, iter.Pos()); + } + + ++i; + ++iter; + } + } + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"DataSource", L"Export:GeneratedMovementTestDataSource") + END_TEST_METHOD_PROPERTIES() + + WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; + WEX::TestExecution::SetVerifyOutput verifyOutputScope{ WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures }; + + unsigned int i{}; + TestData::TryGetValue(L"index", i); // index is produced by the ArrayIndexTaefAdapterSource above + const auto& testCase{ s_movementTests[i] }; + + Log::Comment(NoThrowString().Format(L"[%zu.0] Test case \"%.*s\"", i, testCase.name.size(), testCase.name.data())); + if (testCase.skip) + { + Log::Result(WEX::Logging::TestResults::Result::Skipped); + } + else + { + Microsoft::WRL::ComPtr utr; + int amountMoved; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, testCase.input.start, testCase.input.end)); + THROW_IF_FAILED(utr->Move(testCase.input.unit, testCase.input.moveAmount, &amountMoved)); + + VERIFY_ARE_EQUAL(testCase.expected.moveAmount, amountMoved); + VERIFY_ARE_EQUAL(testCase.expected.start, til::point{ utr->_start }); + VERIFY_ARE_EQUAL(testCase.expected.end, til::point{ utr->_end }); + } + } }; diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index 75235fc94..e5ea81735 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -31,9 +31,6 @@ #if TIL_FEATURE_CONHOSTDXENGINE_ENABLED #include "../../renderer/dx/DxRenderer.hpp" -#else -// Forward-declare this so we don't blow up later. -struct DxEngine; #endif #include "../inc/ServiceLocator.hpp" @@ -214,7 +211,9 @@ void Window::_UpdateSystemMetrics() const const bool useDx = pSettings->GetUseDx(); GdiEngine* pGdiEngine = nullptr; +#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED [[maybe_unused]] DxEngine* pDxEngine = nullptr; +#endif try { #if TIL_FEATURE_CONHOSTDXENGINE_ENABLED diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index 649cc1f01..ad88dc7fa 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -181,7 +181,6 @@ using namespace Microsoft::Console::Types; // First retrieve the new DPI and the current DPI. DWORD const dpiProposed = (WORD)wParam; - DWORD const dpiCurrent = g.dpi; // Now we need to get what the font size *would be* if we had this new DPI. We need to ask the renderer about that. const FontInfo& fiCurrent = ScreenInfo.GetCurrentFont(); diff --git a/src/renderer/base/FontResource.cpp b/src/renderer/base/FontResource.cpp new file mode 100644 index 000000000..38263033a --- /dev/null +++ b/src/renderer/base/FontResource.cpp @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "../inc/FontResource.hpp" + +using namespace Microsoft::Console::Render; + +namespace +{ + // The structures below are based on the Windows 3.0 font file format, which + // was documented in Microsoft Knowledge Base article Q65123. Although no + // longer hosted by Microsoft, it can still be found at the following URL: + // https://web.archive.org/web/20140820153410/http://support.microsoft.com/kb/65123 + + // For now we're only using fixed pitch single color fonts, but the rest + // of the flags are included here for completeness. + static constexpr DWORD DFF_FIXED = 0x0001; + static constexpr DWORD DFF_PROPORTIONAL = 0x0002; + static constexpr DWORD DFF_1COLOR = 0x0010; + static constexpr DWORD DFF_16COLOR = 0x0020; + static constexpr DWORD DFF_256COLOR = 0x0040; + static constexpr DWORD DFF_RGBCOLOR = 0x0080; + + // DRCS soft fonts only require 96 characters at most. + static constexpr size_t CHAR_COUNT = 96; + +#pragma pack(push, 1) + struct GLYPHENTRY + { + WORD geWidth; + DWORD geOffset; + }; + + struct FONTINFO + { + WORD dfVersion; + DWORD dfSize; + CHAR dfCopyright[60]; + WORD dfType; + WORD dfPoints; + WORD dfVertRes; + WORD dfHorizRes; + WORD dfAscent; + WORD dfInternalLeading; + WORD dfExternalLeading; + BYTE dfItalic; + BYTE dfUnderline; + BYTE dfStrikeOut; + WORD dfWeight; + BYTE dfCharSet; + WORD dfPixWidth; + WORD dfPixHeight; + BYTE dfPitchAndFamily; + WORD dfAvgWidth; + WORD dfMaxWidth; + BYTE dfFirstChar; + BYTE dfLastChar; + BYTE dfDefaultChar; + BYTE dfBreakChar; + WORD dfWidthBytes; + DWORD dfDevice; + DWORD dfFace; + DWORD dfBitsPointer; + DWORD dfBitsOffset; + BYTE dfReserved; + DWORD dfFlags; + WORD dfAspace; + WORD dfBspace; + WORD dfCspace; + DWORD dfColorPointer; + DWORD dfReserved1[4]; + GLYPHENTRY dfCharTable[CHAR_COUNT]; + CHAR szFaceName[LF_FACESIZE]; + }; +#pragma pack(pop) +} + +FontResource::FontResource(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint) : + _bitPattern{ bitPattern.begin(), bitPattern.end() }, + _sourceSize{ sourceSize }, + _targetSize{ targetSize }, + _centeringHint{ centeringHint } +{ +} + +void FontResource::SetTargetSize(const til::size targetSize) +{ + if (_targetSize != targetSize) + { + _targetSize = targetSize; + _fontHandle = nullptr; + } +} + +FontResource::operator HFONT() +{ + if (!_fontHandle && !_bitPattern.empty()) + { + _regenerateFont(); + } + return _fontHandle.get(); +} + +void FontResource::_regenerateFont() +{ + const auto targetWidth = _targetSize.width(); + const auto targetHeight = _targetSize.height(); + const auto charSizeInBytes = (targetWidth + 7) / 8 * targetHeight; + + const DWORD fontBitmapSize = charSizeInBytes * CHAR_COUNT; + const DWORD fontResourceSize = sizeof(FONTINFO) + fontBitmapSize; + + auto fontResourceBuffer = std::vector(fontResourceSize); + void* fontResourceBufferPointer = fontResourceBuffer.data(); + auto& fontResource = *static_cast(fontResourceBufferPointer); + + fontResource.dfVersion = 0x300; + fontResource.dfSize = fontResourceSize; + fontResource.dfWeight = FW_NORMAL; + fontResource.dfCharSet = OEM_CHARSET; + fontResource.dfPixWidth = targetWidth; + fontResource.dfPixHeight = targetHeight; + fontResource.dfPitchAndFamily = FIXED_PITCH | FF_DONTCARE; + fontResource.dfAvgWidth = targetWidth; + fontResource.dfMaxWidth = targetWidth; + fontResource.dfFirstChar = L' '; + fontResource.dfLastChar = fontResource.dfFirstChar + CHAR_COUNT - 1; + fontResource.dfFace = offsetof(FONTINFO, szFaceName); + fontResource.dfBitsOffset = sizeof(FONTINFO); + fontResource.dfFlags = DFF_FIXED | DFF_1COLOR; + + // We use an atomic counter to create a locally-unique name for the font. + static std::atomic faceNameCounter; + sprintf_s(fontResource.szFaceName, "WTSOFTFONT%016llX", faceNameCounter++); + + // Each character has a fixed size and position in the font bitmap, but we + // still need to fill in the header table with that information. + for (auto i = 0u; i < std::size(fontResource.dfCharTable); i++) + { + const auto charOffset = fontResource.dfBitsOffset + charSizeInBytes * i; + fontResource.dfCharTable[i].geOffset = charOffset; + fontResource.dfCharTable[i].geWidth = targetWidth; + } + + // Raster fonts aren't generally scalable, so we need to resize the bit + // patterns for the character glyphs to the requested target size, and + // copy the results into the resource structure. + auto fontResourceSpan = gsl::span(fontResourceBuffer); + _resizeBitPattern(fontResourceSpan.subspan(fontResource.dfBitsOffset)); + + DWORD fontCount = 0; + _resourceHandle.reset(AddFontMemResourceEx(&fontResource, fontResourceSize, nullptr, &fontCount)); + LOG_HR_IF_NULL(E_FAIL, _resourceHandle.get()); + + // Once the resource has been registered, we should be able to create the + // font by using the same name and attributes as were set in the resource. + LOGFONTA logFont = {}; + logFont.lfHeight = fontResource.dfPixHeight; + logFont.lfWidth = fontResource.dfPixWidth; + logFont.lfCharSet = fontResource.dfCharSet; + logFont.lfOutPrecision = OUT_RASTER_PRECIS; + logFont.lfPitchAndFamily = fontResource.dfPitchAndFamily; + strcpy_s(logFont.lfFaceName, fontResource.szFaceName); + _fontHandle.reset(CreateFontIndirectA(&logFont)); + LOG_HR_IF_NULL(E_FAIL, _fontHandle.get()); +} + +void FontResource::_resizeBitPattern(gsl::span targetBuffer) +{ + auto sourceWidth = _sourceSize.width(); + auto targetWidth = _targetSize.width(); + const auto sourceHeight = _sourceSize.height(); + const auto targetHeight = _targetSize.height(); + + // If the text in the font is not perfectly centered, the _centeringHint + // gives us the offset needed to correct that misalignment. So to ensure + // that any inserted or deleted columns are evenly spaced around the center + // point of the glyphs, we need to adjust the source and target widths by + // that amount (proportionally) before calculating the scaling increments. + targetWidth -= std::lround((double)_centeringHint * targetWidth / sourceWidth); + sourceWidth -= gsl::narrow_cast(_centeringHint); + + // The way the scaling works is by iterating over the target range, and + // calculating the source offsets that correspond to each target position. + // We achieve that by incrementing the source offset every iteration by an + // integer value that is the quotient of the source and target dimensions. + // Because this is an integer division, we're going to be off by a certain + // fraction on each iteration, so we need to keep track of that accumulated + // error using the modulus of the division. Once the error total exceeds + // the target dimension (more or less), we add another pixel to compensate + // for the error, and reset the error total. + const auto createIncrementFunction = [](const auto sourceDimension, const auto targetDimension) { + const auto increment = sourceDimension / targetDimension; + const auto errorIncrement = sourceDimension % targetDimension * 2; + const auto errorThreshold = targetDimension * 2 - std::min(sourceDimension, targetDimension); + const auto errorReset = targetDimension * 2; + + return [=](auto& errorTotal) { + errorTotal += errorIncrement; + if (errorTotal > errorThreshold) + { + errorTotal -= errorReset; + return increment + 1; + } + return increment; + }; + }; + const auto columnIncrement = createIncrementFunction(sourceWidth, targetWidth); + const auto lineIncrement = createIncrementFunction(sourceHeight, targetHeight); + + // Once we've calculated the scaling increments, taking the centering hint + // into account, we reset the target width back to its original value. + targetWidth = _targetSize.width(); + + auto targetBufferPointer = targetBuffer.begin(); + for (auto ch = 0; ch < CHAR_COUNT; ch++) + { + // Bits are read from the source from left to right - MSB to LSB. The source + // column is a single bit representing the 1-based position. The reason for + // this will become clear in the mask calculation below. + auto sourceColumn = 1 << 16; + auto sourceColumnError = 0; + + // The target format expects the character bitmaps to be laid out in columns + // of 8 bits. So we generate 8 bits from each scanline until we've covered + // the full target height. Then we start again from the top with the next 8 + // bits of the line, until we've covered the full target width. + for (auto targetX = 0; targetX < targetWidth; targetX += 8) + { + auto sourceLine = std::next(_bitPattern.begin(), ch * sourceHeight); + auto sourceLineError = 0; + + // Since we're going to be reading from the same horizontal offset for each + // target line, we save the state here so we can reset it every iteration. + const auto initialSourceColumn = sourceColumn; + const auto initialSourceColumnError = sourceColumnError; + + for (auto targetY = 0; targetY < targetHeight; targetY++) + { + sourceColumn = initialSourceColumn; + sourceColumnError = initialSourceColumnError; + + // For a particular target line, we calculate the span of source lines from + // which it is derived, then OR those values together. We don't want the + // source value to be zero, though, so we must read at least one line. + const auto lineSpan = lineIncrement(sourceLineError); + auto sourceValue = 0; + for (auto i = 0; i < std::max(lineSpan, 1); i++) + { + sourceValue |= sourceLine[i]; + } + std::advance(sourceLine, lineSpan); + + // From the combined value of the source lines, we now need to extract eight + // bits to make up the next byte in the target at the current X offset. + byte targetValue = 0; + for (auto targetBit = 0; targetBit < 8; targetBit++) + { + targetValue <<= 1; + if (targetX + targetBit < targetWidth) + { + // As with the line iteration, we first need to calculate the span of source + // columns from which the target bit is derived. We shift our source column + // position right by that amount to determine the next column position, then + // subtract those two values to obtain a mask. For example, if we're reading + // from columns 6 to 3 (exclusively), the initial column position is 1<<6, + // the next column position is 1<<3, so the mask is 64-8=56, or 00111000. + // Again we don't want this mask to be zero, so if the span is zero, we need + // to shift an additional bit to make sure we cover at least one column. + const auto columnSpan = columnIncrement(sourceColumnError); + const auto nextSourceColumn = sourceColumn >> columnSpan; + const auto sourceMask = sourceColumn - (nextSourceColumn >> (columnSpan ? 0 : 1)); + sourceColumn = nextSourceColumn; + targetValue |= (sourceValue & sourceMask) ? 1 : 0; + } + } + *(targetBufferPointer++) = targetValue; + } + } + } +} diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index f31fff625..a32b4b525 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -36,6 +36,13 @@ HRESULT RenderEngineBase::UpdateTitle(const std::wstring_view newTitle) noexcept return hr; } +HRESULT RenderEngineBase::UpdateSoftFont(const gsl::span /*bitPattern*/, + const SIZE /*cellSize*/, + const size_t /*centeringHint*/) noexcept +{ + return S_FALSE; +} + HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noexcept { return S_FALSE; diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj index f38398cfc..2a9ee32cb 100644 --- a/src/renderer/base/lib/base.vcxproj +++ b/src/renderer/base/lib/base.vcxproj @@ -15,6 +15,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/src/renderer/base/lib/base.vcxproj.filters b/src/renderer/base/lib/base.vcxproj.filters index 1ec046c43..baff50352 100644 --- a/src/renderer/base/lib/base.vcxproj.filters +++ b/src/renderer/base/lib/base.vcxproj.filters @@ -27,6 +27,9 @@ Source Files + + Source Files + Source Files @@ -65,6 +68,9 @@ Header Files\inc + + Header Files\inc + Header Files\inc diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index b53bc6ead..e5103bc3f 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -144,7 +144,7 @@ try }); // A. Prep Colors - RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), true)); + RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), false, true)); // B. Perform Scroll Operations RETURN_IF_FAILED(_PerformScrolling(pEngine)); @@ -466,12 +466,16 @@ void Renderer::TriggerScroll(const COORD* const pcoordDelta) // - void Renderer::TriggerCircling() { + const auto rects = _GetSelectionRects(); + for (IRenderEngine* const pEngine : _rgpEngines) { bool fEngineRequestsRepaint = false; HRESULT hr = pEngine->InvalidateCircling(&fEngineRequestsRepaint); LOG_IF_FAILED(hr); + LOG_IF_FAILED(pEngine->InvalidateSelection(rects)); + if (SUCCEEDED(hr) && fEngineRequestsRepaint) { LOG_IF_FAILED(_PaintFrameForEngine(pEngine)); @@ -526,6 +530,35 @@ void Renderer::TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfo _NotifyPaintFrame(); } +// Routine Description: +// - Called when the active soft font has been updated. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - +void Renderer::UpdateSoftFont(const gsl::span bitPattern, const SIZE cellSize, const size_t centeringHint) +{ + // We reserve PUA code points U+EF20 to U+EF7F for soft fonts, but the range + // that we test for in _IsSoftFontChar will depend on the size of the active + // bitPattern. If it's empty (i.e. no soft font is set), then nothing will + // match, and those code points will be treated the same as everything else. + const auto softFontCharCount = cellSize.cy ? bitPattern.size() / cellSize.cy : 0; + _lastSoftFontChar = _firstSoftFontChar + softFontCharCount - 1; + + for (const auto pEngine : _rgpEngines) + { + LOG_IF_FAILED(pEngine->UpdateSoftFont(bitPattern, cellSize, centeringHint)); + } + TriggerRedrawAll(); +} + +bool Renderer::s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar) +{ + return v.size() == 1 && v.front() >= firstSoftFontChar && v.front() <= lastSoftFontChar; +} + // Routine Description: // - Get the information on what font we would be using if we decided to create a font with the given parameters // - This is for use with speculative calculations. @@ -740,6 +773,8 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, auto color = it->TextAttr(); // Retrieve the first pattern id auto patternIds = _pData->GetPatternId(target); + // Determine whether we're using a soft font. + auto usingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar); // And hold the point where we should start drawing. auto screenPoint = target; @@ -756,8 +791,8 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // Hold onto the current pattern id as well const auto currentPatternId = patternIds; - // Update the drawing brushes with our color. - THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, false)); + // Update the drawing brushes with our color and font usage. + THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, usingSoftFont, false)); // Advance the point by however many columns we've just outputted and reset the accumulator. screenPoint.X += gsl::narrow(cols); @@ -786,15 +821,18 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, { COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; const auto thisPointPatterns = _pData->GetPatternId(thisPoint); - if (color != it->TextAttr() || patternIds != thisPointPatterns) + const auto thisUsingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar); + const auto changedPatternOrFont = patternIds != thisPointPatterns || usingSoftFont != thisUsingSoftFont; + if (color != it->TextAttr() || changedPatternOrFont) { auto newAttr{ it->TextAttr() }; // foreground doesn't matter for runs of spaces (!) // if we trick it . . . we call Paint far fewer times for cmatrix - if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || patternIds != thisPointPatterns) + if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || changedPatternOrFont) { color = newAttr; patternIds = thisPointPatterns; + usingSoftFont = thisUsingSoftFont; break; // vend this run } } @@ -1183,17 +1221,21 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) // Arguments: // - pEngine - Which engine is being updated // - textAttributes - The 16 color foreground/background combination to set +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Alerts that the default brushes are being set which will // impact whether or not to include the hung window/erase window brushes in this operation // and can affect other draw state that wants to know the default color scheme. // (Usually only happens when the default is changed, not when each individual color is swapped in a multi-color run.) // Return Value: // - -[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute textAttributes, const bool isSettingDefaultBrushes) +[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, + const TextAttribute textAttributes, + const bool usingSoftFont, + const bool isSettingDefaultBrushes) { // The last color needs to be each engine's responsibility. If it's local to this function, // then on the next engine we might not update the color. - return pEngine->UpdateDrawingBrushes(textAttributes, _pData, isSettingDefaultBrushes); + return pEngine->UpdateDrawingBrushes(textAttributes, _pData, usingSoftFont, isSettingDefaultBrushes); } // Routine Description: diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 6f8f6d7a8..eb8eadf62 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -65,6 +65,10 @@ namespace Microsoft::Console::Render const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) override; + void UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) override; + [[nodiscard]] HRESULT GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) override; @@ -120,7 +124,10 @@ namespace Microsoft::Console::Render void _PaintOverlays(_In_ IRenderEngine* const pEngine); void _PaintOverlay(IRenderEngine& engine, const RenderOverlay& overlay); - [[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute attr, const bool isSettingDefaultBrushes); + [[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, + const TextAttribute attr, + const bool usingSoftFont, + const bool isSettingDefaultBrushes); [[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); @@ -138,6 +145,10 @@ namespace Microsoft::Console::Render [[nodiscard]] std::optional _GetCursorInfo(); [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); + const size_t _firstSoftFontChar = 0xEF20; + size_t _lastSoftFontChar = 0; + static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); + // Helper functions to diagnose issues with painting and layout. // These are only actually effective/on in Debug builds when the flag is set using an attached debugger. bool _fDebug = false; diff --git a/src/renderer/base/sources.inc b/src/renderer/base/sources.inc index 9aeaf8c65..e3f3d2171 100644 --- a/src/renderer/base/sources.inc +++ b/src/renderer/base/sources.inc @@ -29,6 +29,7 @@ SOURCES = \ ..\FontInfo.cpp \ ..\FontInfoBase.cpp \ ..\FontInfoDesired.cpp \ + ..\FontResource.cpp \ ..\RenderEngineBase.cpp \ ..\renderer.cpp \ ..\thread.cpp \ diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index 0a42ecaea..bf60bce33 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -357,7 +357,7 @@ CATCH_RETURN() _glyphIndices.resize(totalGlyphsArrayCount); } - if (_isEntireTextSimple) + if (_isEntireTextSimple && !_fontRenderData->DidUserSetFeatures()) { // When the entire text is simple, we can skip GetGlyphs and directly retrieve glyph indices and // advances(in font design unit). With the help of font metrics, we can calculate the actual glyph @@ -396,10 +396,18 @@ CATCH_RETURN() std::vector textProps(textLength); std::vector glyphProps(maxGlyphCount); + // Get the features to apply to the font + auto features = _fontRenderData->DefaultFontFeatures(); + DWRITE_FONT_FEATURE* featureList = features.data(); + DWRITE_TYPOGRAPHIC_FEATURES typographicFeatures = { &featureList[0], gsl::narrow(features.size()) }; + DWRITE_TYPOGRAPHIC_FEATURES const* typographicFeaturesPointer = &typographicFeatures; + const uint32_t fontFeatureLengths[] = { textLength }; + // Get the glyphs from the text, retrying if needed. int tries = 0; +#pragma warning(suppress : 26485) // so we can pass in the fontFeatureLengths to GetGlyphs without the analyzer complaining HRESULT hr = S_OK; do { @@ -412,9 +420,9 @@ CATCH_RETURN() &run.script, _localeName.data(), (run.isNumberSubstituted) ? _numberSubstitution.Get() : nullptr, - nullptr, // features - nullptr, // featureLengths - 0, // featureCount + &typographicFeaturesPointer, // features + &fontFeatureLengths[0], // featureLengths + 1, // featureCount maxGlyphCount, // maxGlyphCount &_glyphClusters.at(textStart), &textProps.at(0), @@ -462,9 +470,9 @@ CATCH_RETURN() (run.bidiLevel & 1), // isRightToLeft &run.script, _localeName.data(), - nullptr, // features - nullptr, // featureRangeLengths - 0, // featureRanges + &typographicFeaturesPointer, // features + &fontFeatureLengths[0], // featureLengths + 1, // featureCount &_glyphAdvances.at(glyphStart), &_glyphOffsets.at(glyphStart)); @@ -1264,29 +1272,71 @@ CATCH_RETURN(); fallback = _fontRenderData->SystemFontFallback(); } - // Walk through and analyze the entire string - while (textLength > 0) + ::Microsoft::WRL::ComPtr fallback1; + ::Microsoft::WRL::ComPtr format3; + + // If the OS supports IDWriteFontFallback1 and IDWriteTextFormat3, we can use the + // newer MapCharacters to apply axes of variation to the font + if (!FAILED(_formatInUse->QueryInterface(IID_PPV_ARGS(&format3))) && !FAILED(fallback->QueryInterface(IID_PPV_ARGS(&fallback1)))) { - UINT32 mappedLength = 0; - ::Microsoft::WRL::ComPtr mappedFont; - FLOAT scale = 0.0f; + const auto axesVector = _fontRenderData->GetAxisVector(weight, stretch, style, format3.Get()); + // Walk through and analyze the entire string + while (textLength > 0) + { + UINT32 mappedLength = 0; + ::Microsoft::WRL::ComPtr mappedFont; + FLOAT scale = 0.0f; - fallback->MapCharacters(source, - textPosition, - textLength, - collection.Get(), - familyName.data(), - weight, - style, - stretch, - &mappedLength, - &mappedFont, - &scale); + fallback1->MapCharacters(source, + textPosition, + textLength, + collection.Get(), + familyName.data(), + axesVector.data(), + gsl::narrow(axesVector.size()), + &mappedLength, + &scale, + &mappedFont); - RETURN_IF_FAILED(_SetMappedFont(textPosition, mappedLength, mappedFont.Get(), scale)); + RETURN_IF_FAILED(_SetMappedFontFace(textPosition, mappedLength, mappedFont, scale)); - textPosition += mappedLength; - textLength -= mappedLength; + textPosition += mappedLength; + textLength -= mappedLength; + } + } + else + { + // The chunk of code below is very similar to the one above, unfortunately this needs + // to stay for Win7 compatibility reasons. It is also not possible to combine the two + // because they call different versions of MapCharacters + + // Walk through and analyze the entire string + while (textLength > 0) + { + UINT32 mappedLength = 0; + ::Microsoft::WRL::ComPtr mappedFont; + FLOAT scale = 0.0f; + + fallback->MapCharacters(source, + textPosition, + textLength, + collection.Get(), + familyName.data(), + weight, + style, + stretch, + &mappedLength, + &mappedFont, + &scale); + + RETURN_LAST_ERROR_IF(!mappedFont); + ::Microsoft::WRL::ComPtr face; + RETURN_IF_FAILED(mappedFont->CreateFontFace(&face)); + RETURN_IF_FAILED(_SetMappedFontFace(textPosition, mappedLength, face, scale)); + + textPosition += mappedLength; + textLength -= mappedLength; + } } } CATCH_RETURN(); @@ -1300,14 +1350,14 @@ CATCH_RETURN(); // Arguments: // - textPosition - the index to start the substring operation // - textLength - the length of the substring operation -// - font - the font that applies to the substring range +// - fontFace - the fontFace that applies to the substring range // - scale - the scale of the font to apply // Return Value: // - S_OK or appropriate STL/GSL failure code. -[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFont(UINT32 textPosition, - UINT32 textLength, - _In_ IDWriteFont* const font, - FLOAT const scale) +[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFontFace(UINT32 textPosition, + UINT32 textLength, + const ::Microsoft::WRL::ComPtr& fontFace, + FLOAT const scale) { try { @@ -1317,14 +1367,9 @@ CATCH_RETURN(); { auto& run = _FetchNextRun(textLength); - if (font != nullptr) + if (fontFace != nullptr) { - // Get font face from font metadata - ::Microsoft::WRL::ComPtr face; - RETURN_IF_FAILED(font->CreateFontFace(&face)); - - // QI for Face5 interface from base face interface, store into run - RETURN_IF_FAILED(face.As(&run.fontFace)); + RETURN_IF_FAILED(fontFace.As(&run.fontFace)); } else { diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index 2ac04278b..d1cbabfdb 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -126,7 +126,7 @@ namespace Microsoft::Console::Render void _OrderRuns(); [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength); - [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFont(UINT32 textPosition, UINT32 textLength, IDWriteFont* const font, FLOAT const scale); + [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFontFace(UINT32 textPosition, UINT32 textLength, const ::Microsoft::WRL::ComPtr& fontFace, FLOAT const scale); [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeBoxDrawing(gsl::not_null const source, UINT32 textPosition, UINT32 textLength); [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetBoxEffect(UINT32 textPosition, UINT32 textLength); diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index c91b95dd1..fb3e75138 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -12,6 +12,7 @@ static constexpr float POINTS_PER_INCH = 72.0f; static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" }; static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us"; +static constexpr size_t TAG_LENGTH = 4; using namespace Microsoft::Console::Render; @@ -93,6 +94,11 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _defaultFontInfo.GetStretch(); } +[[nodiscard]] const std::vector& DxFontRenderData::DefaultFontFeatures() const noexcept +{ + return _featureVector; +} + [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() { return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); @@ -178,7 +184,7 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr // - dpi - The DPI of the screen // Return Value: // - S_OK or relevant DirectX error -[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi) noexcept +[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi, const std::unordered_map& features, const std::unordered_map& axes) noexcept { try { @@ -193,6 +199,9 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL); + _SetFeatures(features); + _SetAxes(axes); + _BuildFontRenderData(desired, actual, dpi); } CATCH_RETURN(); @@ -441,6 +450,188 @@ try } CATCH_RETURN() +// Routine Description: +// - Returns whether the user set or updated any of the font features to be applied +bool DxFontRenderData::DidUserSetFeatures() const noexcept +{ + return _didUserSetFeatures; +} + +// Routine Description: +// - Updates our internal map of font features with the given features +// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the feature changes +// to take place +// Arguments: +// - features - the features to update our map with +void DxFontRenderData::_SetFeatures(const std::unordered_map& features) +{ + // Populate the feature map with the standard list first + std::unordered_map featureMap{ + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'a', 'l', 't'), 1 }, // Contextual Alternates + { DWRITE_MAKE_FONT_FEATURE_TAG('l', 'i', 'g', 'a'), 1 }, // Standard Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'l', 'i', 'g'), 1 }, // Contextual Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'), 1 } // Kerning + }; + + // Update our feature map with the provided features + if (!features.empty()) + { + for (const auto [tag, param] : features) + { + if (tag.length() == TAG_LENGTH) + { + featureMap.insert_or_assign(DWRITE_MAKE_FONT_FEATURE_TAG(til::at(tag, 0), til::at(tag, 1), til::at(tag, 2), til::at(tag, 3)), param); + } + } + _didUserSetFeatures = true; + } + else + { + _didUserSetFeatures = false; + } + + // Convert the data to DWRITE_FONT_FEATURE and store it in a vector for CustomTextLayout + _featureVector.clear(); + for (const auto [tag, param] : featureMap) + { + _featureVector.push_back(DWRITE_FONT_FEATURE{ tag, param }); + } +} + +// Routine Description: +// - Updates our internal map of font axes with the given axes +// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the axes changes +// to take place +// Arguments: +// - axes - the axes to update our map with +void DxFontRenderData::_SetAxes(const std::unordered_map& axes) +{ + _axesVector.clear(); + + // Update our axis map with the provided axes +#pragma warning(suppress : 26445) // the analyzer doesn't like reference to string_view + for (const auto& [axis, value] : axes) + { + if (axis.length() == TAG_LENGTH) + { + const auto dwriteTag = DWRITE_MAKE_FONT_AXIS_TAG(til::at(axis, 0), til::at(axis, 1), til::at(axis, 2), til::at(axis, 3)); + _axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ dwriteTag, value }); + } + } +} + +// Method Description: +// - Converts a DWRITE_FONT_STRETCH enum into the corresponding float value to +// create a DWRITE_FONT_AXIS_VALUE with +// Arguments: +// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value +// Return value: +// - The float value corresponding to the passed in fontStretch +float DxFontRenderData::_FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept +{ + // 10 elements from DWRITE_FONT_STRETCH_UNDEFINED (0) to DWRITE_FONT_STRETCH_ULTRA_EXPANDED (9) + static constexpr auto fontStretchEnumToVal = std::array{ 100.0f, 50.0f, 62.5f, 75.0f, 87.5f, 100.0f, 112.5f, 125.0f, 150.0f, 200.0f }; + + if (gsl::narrow_cast(fontStretch) > fontStretchEnumToVal.size()) + { + fontStretch = DWRITE_FONT_STRETCH_NORMAL; + } + + return til::at(fontStretchEnumToVal, fontStretch); +} + +// Method Description: +// - Converts a DWRITE_FONT_STYLE enum into the corresponding float value to +// create a DWRITE_FONT_AXIS_VALUE with +// Arguments: +// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value +// Return value: +// - The float value corresponding to the passed in fontStyle +float DxFontRenderData::_FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept +{ + // DWRITE_FONT_STYLE_NORMAL (0), DWRITE_FONT_STYLE_OBLIQUE (1), DWRITE_FONT_STYLE_ITALIC (2) + static constexpr auto fontStyleEnumToVal = std::array{ 0.0f, -20.0f, -12.0f }; + + // Both DWRITE_FONT_STYLE_OBLIQUE and DWRITE_FONT_STYLE_ITALIC default to having slant. + // Though an italic font technically need not have slant (there exist upright ones), the + // vast majority of italic fonts are also slanted. Ideally the slant comes from the + // 'slnt' value in the STAT or fvar table, or the post table italic angle. + + if (gsl::narrow_cast(fontStyle) > fontStyleEnumToVal.size()) + { + fontStyle = DWRITE_FONT_STYLE_NORMAL; + } + + return til::at(fontStyleEnumToVal, fontStyle); +} + +// Method Description: +// - Fill any missing axis values that might be known but were unspecified, such as omitting +// the 'wght' axis tag but specifying the old DWRITE_FONT_WEIGHT enum +// - This function will only be called with a valid IDWriteTextFormat3 +// (on platforms where IDWriteTextFormat3 is supported) +// Arguments: +// - fontWeight: the old DWRITE_FONT_WEIGHT enum to be converted into an axis value +// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value +// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value +// - fontSize: the number to convert into an axis value +// - format: the IDWriteTextFormat3 to get the defined axes from +// Return value: +// - The fully formed axes vector +#pragma warning(suppress : 26429) // the analyzer doesn't detect that our FAIL_FAST_IF_NULL macro \ + // checks format for nullness +std::vector DxFontRenderData::GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight, + const DWRITE_FONT_STRETCH fontStretch, + const DWRITE_FONT_STYLE fontStyle, + IDWriteTextFormat3* format) +{ + FAIL_FAST_IF_NULL(format); + + const auto axesCount = format->GetFontAxisValueCount(); + std::vector axesVector; + axesVector.resize(axesCount); + format->GetFontAxisValues(axesVector.data(), axesCount); + + auto axisTagPresence = AxisTagPresence::None; + for (const auto& fontAxisValue : axesVector) + { + switch (fontAxisValue.axisTag) + { + case DWRITE_FONT_AXIS_TAG_WEIGHT: + WI_SetFlag(axisTagPresence, AxisTagPresence::Weight); + break; + case DWRITE_FONT_AXIS_TAG_WIDTH: + WI_SetFlag(axisTagPresence, AxisTagPresence::Width); + break; + case DWRITE_FONT_AXIS_TAG_ITALIC: + WI_SetFlag(axisTagPresence, AxisTagPresence::Italic); + break; + case DWRITE_FONT_AXIS_TAG_SLANT: + WI_SetFlag(axisTagPresence, AxisTagPresence::Slant); + break; + } + } + + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Weight)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, gsl::narrow(fontWeight) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Width)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WIDTH, _FontStretchToWidthAxisValue(fontStretch) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Italic)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, (fontStyle == DWRITE_FONT_STYLE_ITALIC ? 1.0f : 0.0f) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Slant)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, _FontStyleToSlantFixedAxisValue(fontStyle) }); + } + + return axesVector; +} + // Routine Description: // - Build the needed data for rendering according to the font used // Arguments: @@ -649,5 +840,14 @@ Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(con _fontSize, localeName.data(), &format)); + + // If the OS supports IDWriteTextFormat3, set the font axes + ::Microsoft::WRL::ComPtr format3; + if (!_axesVector.empty() && !FAILED(format->QueryInterface(IID_PPV_ARGS(&format3)))) + { + DWRITE_FONT_AXIS_VALUE const* axesList = _axesVector.data(); + format3->SetFontAxisValues(axesList, gsl::narrow(_axesVector.size())); + } + return format; } diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index 2cf5fc7be..379fd6036 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -16,6 +16,16 @@ namespace Microsoft::Console::Render { + enum class AxisTagPresence : BYTE + { + None = 0x00, + Weight = 0x01, + Width = 0x02, + Italic = 0x04, + Slant = 0x08, + }; + DEFINE_ENUM_FLAG_OPERATORS(AxisTagPresence); + class DxFontRenderData { public: @@ -51,6 +61,9 @@ namespace Microsoft::Console::Render // The stretch of default font [[nodiscard]] DWRITE_FONT_STRETCH DefaultFontStretch() noexcept; + // The font features of the default font + [[nodiscard]] const std::vector& DefaultFontFeatures() const noexcept; + // The DirectWrite format object representing the size and other text properties to be applied (by default) [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat(); @@ -70,19 +83,37 @@ namespace Microsoft::Console::Render DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch); - [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi) noexcept; + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi, const std::unordered_map& features = {}, const std::unordered_map& axes = {}) noexcept; [[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept; + bool DidUserSetFeatures() const noexcept; + + std::vector GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight, + const DWRITE_FONT_STRETCH fontStretch, + const DWRITE_FONT_STYLE fontStyle, + IDWriteTextFormat3* format); + private: using FontAttributeMapKey = uint32_t; + bool _didUserSetFeatures{ false }; + // The font features to apply to the text + std::vector _featureVector; + + // The font axes to apply to the text + std::vector _axesVector; + // We use this to identify font variants with different attributes. static FontAttributeMapKey _ToMapKey(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch) noexcept { return (weight << 16) | (style << 8) | stretch; }; + void _SetFeatures(const std::unordered_map& features); + void _SetAxes(const std::unordered_map& axes); + float _FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept; + float _FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept; void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi); Microsoft::WRL::ComPtr _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName); diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 03904814e..d01d99364 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -61,7 +61,6 @@ using namespace Microsoft::Console::Types; // TODO GH 2683: The default constructor should not throw. DxEngine::DxEngine() : RenderEngineBase(), - _invalidateFullRows{ true }, _pool{ til::pmr::get_default_resource() }, _invalidMap{ &_pool }, _invalidScroll{}, @@ -95,6 +94,7 @@ DxEngine::DxEngine() : _dpi{ USER_DEFAULT_SCREEN_DPI }, _scale{ 1.0f }, _prevScale{ 1.0f }, + _intenseIsBold{ true }, _chainMode{ SwapChainMode::ForComposition }, _customLayout{}, _customRenderer{ ::Microsoft::WRL::Make() }, @@ -963,8 +963,6 @@ CATCH_RETURN() try { _sizeTarget = Pixels; - - _invalidMap.resize(_sizeTarget / _fontRenderData->GlyphCell(), true); return S_OK; } CATCH_RETURN(); @@ -1035,6 +1033,17 @@ try } CATCH_LOG() +void DxEngine::SetIntenseIsBold(bool enable) noexcept +try +{ + if (_intenseIsBold != enable) + { + _intenseIsBold = enable; + LOG_IF_FAILED(InvalidateAll()); + } +} +CATCH_LOG() + HANDLE DxEngine::GetSwapChainHandle() { if (!_swapChainHandle) @@ -1047,14 +1056,10 @@ HANDLE DxEngine::GetSwapChainHandle() void DxEngine::_InvalidateRectangle(const til::rectangle& rc) { - auto invalidate = rc; - - if (_invalidateFullRows) - { - invalidate = til::rectangle{ til::point{ static_cast(0), rc.top() }, til::size{ _invalidMap.size().width(), rc.height() } }; - } - - _invalidMap.set(invalidate); + const auto size = _invalidMap.size(); + const auto topLeft = til::point{ 0, std::min(size.height(), rc.top()) }; + const auto bottomRight = til::point{ size.width(), std::min(size.height(), rc.bottom()) }; + _invalidMap.set({ topLeft, bottomRight }); } bool DxEngine::_IsAllInvalid() const noexcept @@ -1273,7 +1278,7 @@ try // so the entire frame is repainted. if (_FullRepaintNeeded()) { - _invalidMap.set_all(); + RETURN_IF_FAILED(InvalidateAll()); } if (TraceLoggingProviderEnabled(g_hDxRenderProvider, WINEVENT_LEVEL_VERBOSE, TIL_KEYWORD_TRACE)) @@ -1292,6 +1297,7 @@ try if (_isEnabled) { const auto clientSize = _GetClientSize(); + const auto glyphCellSize = _fontRenderData->GlyphCell(); // If we don't have device resources or if someone has requested that we // recreate the device... then make new resources. (Create will dump the old ones.) @@ -1321,9 +1327,12 @@ try // And persist the new size. _displaySizePixels = clientSize; + } - // Mark this as the first frame on the new target. We can't use incremental drawing on the first frame. - _firstFrame = true; + if (const auto size = clientSize / glyphCellSize; size != _invalidMap.size()) + { + _invalidMap.resize(size); + RETURN_IF_FAILED(InvalidateAll()); } _d2dDeviceContext->BeginDraw(); @@ -1341,7 +1350,7 @@ try _ShouldForceGrayscaleAA(), _dwriteFactory.Get(), spacing, - _fontRenderData->GlyphCell(), + glyphCellSize, _d2dDeviceContext->GetSize(), std::nullopt, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); @@ -1910,11 +1919,13 @@ CATCH_RETURN() // Arguments: // - textAttributes - Text attributes to use for the brush color // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Lets us know that these are the default brushes to paint the swapchain background or selection // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool /*usingSoftFont*/, const bool isSettingDefaultBrushes) noexcept { // GH#5098: If we're rendering with cleartype text, we need to always render @@ -1961,7 +1972,7 @@ CATCH_RETURN() if (_drawingContext) { _drawingContext->forceGrayscaleAA = _ShouldForceGrayscaleAA(); - _drawingContext->useBoldFont = textAttributes.IsBold(); + _drawingContext->useBoldFont = _intenseIsBold && textAttributes.IsBold(); _drawingContext->useItalicFont = textAttributes.IsItalic(); } @@ -1978,15 +1989,30 @@ CATCH_RETURN() // Routine Description: // - Updates the font used for drawing +// - This is the version that complies with the IRenderEngine interface // Arguments: // - pfiFontInfoDesired - Information specifying the font that is requested // - fiFontInfo - Filled with the nearest font actually chosen for drawing // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept +{ + return UpdateFont(pfiFontInfoDesired, fiFontInfo, {}, {}); +} + +// Routine Description: +// - Updates the font used for drawing +// Arguments: +// - pfiFontInfoDesired - Information specifying the font that is requested +// - fiFontInfo - Filled with the nearest font actually chosen for drawing +// - features - The map of font features to use +// - axes - The map of font axes to use +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept try { - RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi)); + RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi, features, axes)); // Prepare the text layout. _customLayout = WRL::Make(_fontRenderData.get()); diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index cbd34ea3b..9c96e7f06 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -107,8 +107,10 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; @@ -127,6 +129,7 @@ namespace Microsoft::Console::Render void SetSelectionBackground(const COLORREF color, const float alpha = 0.5f) noexcept; void SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept; void SetDefaultTextBackgroundOpacity(const float opacity) noexcept; + void SetIntenseIsBold(const bool opacity) noexcept; void UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept; @@ -168,7 +171,6 @@ namespace Microsoft::Console::Render uint16_t _hyperlinkHoveredId; bool _firstFrame; - bool _invalidateFullRows; std::pmr::unsynchronized_pool_resource _pool; til::pmr::bitmap _invalidMap; til::point _invalidScroll; @@ -256,6 +258,7 @@ namespace Microsoft::Console::Render D2D1_TEXT_ANTIALIAS_MODE _antialiasingMode; float _defaultTextBackgroundOpacity; + bool _intenseIsBold; // DirectX constant buffers need to be a multiple of 16; align to pad the size. __declspec(align(16)) struct diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 230b09c4f..4caf62ad6 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -15,6 +15,7 @@ Author(s): #pragma once #include "../inc/RenderEngineBase.hpp" +#include "../inc/FontResource.hpp" namespace Microsoft::Console::Render { @@ -61,9 +62,13 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; [[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; @@ -95,6 +100,7 @@ namespace Microsoft::Console::Render HFONT _hfont; HFONT _hfontItalic; TEXTMETRICW _tmFontMetrics; + FontResource _softFont; static const size_t s_cPolyTextCache = 80; POLYTEXTW _pPolyText[s_cPolyTextCache]; @@ -130,7 +136,14 @@ namespace Microsoft::Console::Render COLORREF _lastFg; COLORREF _lastBg; - bool _lastFontItalic; + + enum class FontType : size_t + { + Default, + Italic, + Soft + }; + FontType _lastFontType; XFORM _currentLineTransform; LineRendition _currentLineRendition; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 089e9777e..347135974 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -336,6 +336,9 @@ using namespace Microsoft::Console::Render; auto& polyWidth = _polyWidths.emplace_back(); polyWidth.reserve(cchLine); + // If we have a soft font, we only use the character's lower 7 bits. + const auto softFontCharMask = _lastFontType == FontType::Soft ? L'\x7F' : ~0; + // Sum up the total widths the entire line/run is expected to take while // copying the pixel widths into a structure to direct GDI how many pixels to use per character. size_t cchCharWidths = 0; @@ -347,6 +350,7 @@ using namespace Microsoft::Console::Render; const auto text = cluster.GetText(); polyString += text; + polyString.back() &= softFontCharMask; polyWidth.push_back(gsl::narrow(cluster.GetColumns()) * coordFontSize.X); cchCharWidths += polyWidth.back(); polyWidth.append(text.size() - 1, 0); diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 3311e7585..04a5df7d5 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -29,7 +29,7 @@ GdiEngine::GdiEngine() : _fInvalidRectUsed(false), _lastFg(INVALID_COLOR), _lastBg(INVALID_COLOR), - _lastFontItalic(false), + _lastFontType(FontType::Default), _currentLineTransform(IDENTITY_XFORM), _currentLineRendition(LineRendition::SingleWidth), _fPaintStarted(false), @@ -148,8 +148,8 @@ GdiEngine::~GdiEngine() LOG_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, _hfont)); } - // Record the fact that the selected font is not italic. - _lastFontItalic = false; + // Record the fact that the selected font is the default. + _lastFontType = FontType::Default; if (nullptr != hdcRealWindow) { @@ -269,12 +269,14 @@ GdiEngine::~GdiEngine() // Arguments: // - textAttributes - Text attributes to use for the brush color // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Lets us know that the default brushes are being set so we can update the DC background // and the hung app background painting color // Return Value: // - S_OK if set successfully or relevant GDI error via HRESULT. [[nodiscard]] HRESULT GdiEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept { RETURN_IF_FAILED(_FlushBufferLines()); @@ -304,12 +306,25 @@ GdiEngine::~GdiEngine() RETURN_IF_FAILED(s_SetWindowLongWHelper(_hwndTargetWindow, GWL_CONSOLE_BKCOLOR, colorBackground)); } - // If the italic attribute has changed, select an appropriate font variant. - const auto fontItalic = textAttributes.IsItalic(); - if (fontItalic != _lastFontItalic) + // If the font type has changed, select an appropriate font variant or soft font. + const auto usingItalicFont = textAttributes.IsItalic(); + const auto fontType = usingSoftFont ? FontType::Soft : usingItalicFont ? FontType::Italic : FontType::Default; + if (fontType != _lastFontType) { - SelectFont(_hdcMemoryContext, fontItalic ? _hfontItalic : _hfont); - _lastFontItalic = fontItalic; + switch (fontType) + { + case FontType::Soft: + SelectFont(_hdcMemoryContext, _softFont); + break; + case FontType::Italic: + SelectFont(_hdcMemoryContext, _hfontItalic); + break; + case FontType::Default: + default: + SelectFont(_hdcMemoryContext, _hfont); + break; + } + _lastFontType = fontType; } return S_OK; @@ -331,8 +346,8 @@ GdiEngine::~GdiEngine() // Select into DC RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, hFont.get())); - // Record the fact that the selected font is not italic. - _lastFontItalic = false; + // Record the fact that the selected font is the default. + _lastFontType = FontType::Default; // Save off the font metrics for various other calculations RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(_hdcMemoryContext, &_tmFontMetrics))); @@ -419,11 +434,39 @@ GdiEngine::~GdiEngine() _isTrueTypeFont = Font.IsTrueTypeFont(); _fontCodepage = Font.GetCodePage(); + // Inform the soft font of the change in size. + _softFont.SetTargetSize(_GetFontSize()); + LOG_IF_FAILED(InvalidateAll()); return S_OK; } +// Routine Description: +// - This method will replace the active soft font with the given bit pattern. +// Arguments: +// - bitPattern - An array of scanlines representing all the glyphs in the font. +// - cellSize - The cell size for an individual glyph. +// - centeringHint - The horizontal extent that glyphs are offset from center. +// Return Value: +// - S_OK if successful. E_FAIL if there was an error. +[[nodiscard]] HRESULT GdiEngine::UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + // If the soft font is currently selected, replace it with the default font. + if (_lastFontType == FontType::Soft) + { + RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, _hfont)); + _lastFontType = FontType::Default; + } + + // Create a new font resource with the updated pattern, or delete if empty. + _softFont = { bitPattern, cellSize, _GetFontSize(), centeringHint }; + + return S_OK; +} + // Routine Description: // - This method will modify the DPI we're using for scaling calculations. // Arguments: diff --git a/src/renderer/inc/FontResource.hpp b/src/renderer/inc/FontResource.hpp new file mode 100644 index 000000000..aa159ed34 --- /dev/null +++ b/src/renderer/inc/FontResource.hpp @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontResource.hpp + +Abstract: +- This manages the construction of in-memory font resources for the VT soft fonts. +--*/ + +#pragma once + +namespace wil +{ + typedef unique_any unique_hfontresource; +} + +namespace Microsoft::Console::Render +{ + class FontResource + { + public: + FontResource(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint); + FontResource() = default; + ~FontResource() = default; + FontResource& operator=(FontResource&&) = default; + void SetTargetSize(const til::size targetSize); + operator HFONT(); + + private: + void _regenerateFont(); + void _resizeBitPattern(gsl::span targetBuffer); + + std::vector _bitPattern; + til::size _sourceSize; + til::size _targetSize; + size_t _centeringHint{ 0 }; + wil::unique_hfontresource _resourceHandle; + wil::unique_hfont _fontHandle; + }; +} diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 3d6631e41..bcc98dfc4 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -96,9 +96,13 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept = 0; + [[nodiscard]] virtual HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDpi(const int iDpi) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept = 0; diff --git a/src/renderer/inc/IRenderer.hpp b/src/renderer/inc/IRenderer.hpp index 79058c5ae..a3894bdb7 100644 --- a/src/renderer/inc/IRenderer.hpp +++ b/src/renderer/inc/IRenderer.hpp @@ -50,6 +50,10 @@ namespace Microsoft::Console::Render const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) = 0; + virtual void UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) = 0; + [[nodiscard]] virtual HRESULT GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) = 0; diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 725347d82..6ad59fec3 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -38,6 +38,10 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; + [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; [[nodiscard]] HRESULT ResetLineTransform() noexcept override; diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index ba7ca8e5f..2d5d17196 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -364,11 +364,13 @@ CATCH_RETURN(); // Arguments: // - textAttributes - // - pData - +// - usingSoftFont - // - isSettingDefaultBrushes - // Return Value: // - S_FALSE since we do nothing [[nodiscard]] HRESULT UiaEngine::UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { return S_FALSE; diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 3ead4673b..6591579c5 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -62,6 +62,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/renderer/vt/Xterm256Engine.cpp b/src/renderer/vt/Xterm256Engine.cpp index 91e220c33..4edece715 100644 --- a/src/renderer/vt/Xterm256Engine.cpp +++ b/src/renderer/vt/Xterm256Engine.cpp @@ -20,12 +20,14 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe, // Arguments: // - textAttributes - Text attributes to use for the colors and character rendition // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes: indicates if we should change the background color of // the window. Unused for VT // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT Xterm256Engine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { RETURN_IF_FAILED(VtEngine::_RgbUpdateDrawingBrushes(textAttributes)); diff --git a/src/renderer/vt/Xterm256Engine.hpp b/src/renderer/vt/Xterm256Engine.hpp index b1ed02332..4e5913e07 100644 --- a/src/renderer/vt/Xterm256Engine.hpp +++ b/src/renderer/vt/Xterm256Engine.hpp @@ -30,6 +30,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT ManuallyClearScrollback() noexcept override; diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index a948acd82..1c09225fa 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -137,12 +137,14 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // Arguments: // - textAttributes - Text attributes to use for the colors and character rendition // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes: indicates if we should change the background color of // the window. Unused for VT // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT XtermEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { // The base xterm mode only knows about 16 colors diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index 1dcf2d173..a46458ad6 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -40,6 +40,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, const COORD coord, diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 5fc185338..93ad4ade6 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -163,8 +163,6 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, // - S_OK or suitable HRESULT error from writing pipe. [[nodiscard]] HRESULT VtEngine::_WriteTerminalAscii(const std::wstring_view wstr) noexcept { - const size_t cchActual = wstr.length(); - std::string needed; needed.reserve(wstr.size()); diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index f7b0a788b..bb8f0413f 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -75,6 +75,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, _Out_ FontInfo& pfiFontInfo) noexcept override; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index ed2c027ee..ba67819ab 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -307,6 +307,7 @@ bool WddmConEngine::IsInitialized() [[nodiscard]] HRESULT WddmConEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, bool const /*isSettingDefaultBrushes*/) noexcept { _currentLegacyColorAttribute = textAttributes.GetLegacyAttributes(); diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 6898f1ece..57663d5cb 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -52,6 +52,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, bool const isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/server/IoDispatchers.cpp b/src/server/IoDispatchers.cpp index 4044c41ca..63f9dc231 100644 --- a/src/server/IoDispatchers.cpp +++ b/src/server/IoDispatchers.cpp @@ -431,7 +431,7 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API CommandHistory::s_Free((HANDLE)ProcessData); gci.ProcessHandleList.FreeProcessData(ProcessData); } - + Tracing::s_TraceConsoleAttachDetach(ProcessData, true); UnlockConsole(); diff --git a/src/server/ProcessHandle.cpp b/src/server/ProcessHandle.cpp index f4d0a1d7f..7c2adfc64 100644 --- a/src/server/ProcessHandle.cpp +++ b/src/server/ProcessHandle.cpp @@ -79,8 +79,8 @@ const HANDLE ConsoleProcessHandle::GetRawHandle() const // - The creation time is lazily populated on first call const ULONG64 ConsoleProcessHandle::GetProcessCreationTime() const { - if (_processCreationTime == 0 && _hProcess != nullptr) { - + if (_processCreationTime == 0 && _hProcess != nullptr) + { FILETIME ftCreationTime, ftDummyTime = { 0 }; ULARGE_INTEGER creationTime = { 0 }; @@ -88,7 +88,8 @@ const ULONG64 ConsoleProcessHandle::GetProcessCreationTime() const &ftCreationTime, &ftDummyTime, &ftDummyTime, - &ftDummyTime)) { + &ftDummyTime)) + { creationTime.HighPart = ftCreationTime.dwHighDateTime; creationTime.LowPart = ftCreationTime.dwLowDateTime; } diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 1938791d4..b22634637 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -434,6 +434,46 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes DependsOnMode }; + enum class DrcsEraseControl : size_t + { + AllChars = 0, + ReloadedChars = 1, + AllRenditions = 2 + }; + + enum class DrcsCellMatrix : size_t + { + Default = 0, + Invalid = 1, + Size5x10 = 2, + Size6x10 = 3, + Size7x10 = 4 + }; + + enum class DrcsFontSet : size_t + { + Default = 0, + Size80x24 = 1, + Size132x24 = 2, + Size80x36 = 11, + Size132x36 = 12, + Size80x48 = 21, + Size132x48 = 22 + }; + + enum class DrcsFontUsage : size_t + { + Default = 0, + Text = 1, + FullCell = 2 + }; + + enum class DrcsCharsetSize : size_t + { + Size94 = 0, + Size96 = 1 + }; + constexpr short s_sDECCOLMSetColumns = 132; constexpr short s_sDECCOLMResetColumns = 80; diff --git a/src/terminal/adapter/FontBuffer.cpp b/src/terminal/adapter/FontBuffer.cpp new file mode 100644 index 000000000..688325b27 --- /dev/null +++ b/src/terminal/adapter/FontBuffer.cpp @@ -0,0 +1,603 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "FontBuffer.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +FontBuffer::FontBuffer() noexcept +{ + SetEraseControl(DispatchTypes::DrcsEraseControl::AllRenditions); +}; + +bool FontBuffer::SetEraseControl(const DispatchTypes::DrcsEraseControl eraseControl) noexcept +{ + switch (eraseControl) + { + case DispatchTypes::DrcsEraseControl::AllChars: + case DispatchTypes::DrcsEraseControl::AllRenditions: + // Setting the current cell matrix to an invalid value will guarantee + // that it's different from the pending cell matrix, and any change in + // the font attributes will force the buffer to be cleared. + _cellMatrix = DispatchTypes::DrcsCellMatrix::Invalid; + return true; + case DispatchTypes::DrcsEraseControl::ReloadedChars: + return true; + default: + return false; + } +} + +bool FontBuffer::SetAttributes(const DispatchTypes::DrcsCellMatrix cellMatrix, + const VTParameter cellHeight, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage) noexcept +{ + auto valid = true; + + if (valid) + { + // We don't yet support screen sizes in which the font is horizontally + // or vertically compressed, so there is not much value in storing a + // separate font for each of the screen sizes. However, we still need + // to use these values to determine the cell size for which the font + // was originally targeted, so we can resize it appropriately. + switch (fontSet) + { + case DispatchTypes::DrcsFontSet::Default: + case DispatchTypes::DrcsFontSet::Size80x24: + _columnsPerPage = 80; + _linesPerPage = 24; + break; + case DispatchTypes::DrcsFontSet::Size80x36: + _columnsPerPage = 80; + _linesPerPage = 36; + break; + case DispatchTypes::DrcsFontSet::Size80x48: + _columnsPerPage = 80; + _linesPerPage = 48; + break; + case DispatchTypes::DrcsFontSet::Size132x24: + _columnsPerPage = 132; + _linesPerPage = 24; + break; + case DispatchTypes::DrcsFontSet::Size132x36: + _columnsPerPage = 132; + _linesPerPage = 36; + break; + case DispatchTypes::DrcsFontSet::Size132x48: + _columnsPerPage = 132; + _linesPerPage = 48; + break; + default: + valid = false; + break; + } + } + + if (valid) + { + switch (fontUsage) + { + case DispatchTypes::DrcsFontUsage::Default: + case DispatchTypes::DrcsFontUsage::Text: + _isTextFont = true; + break; + case DispatchTypes::DrcsFontUsage::FullCell: + _isTextFont = false; + break; + default: + valid = false; + break; + } + } + + if (valid) + { + switch (cellMatrix) + { + case DispatchTypes::DrcsCellMatrix::Invalid: + valid = false; + break; + case DispatchTypes::DrcsCellMatrix::Size5x10: + // Size 5x10 is only valid for text fonts. + valid = _isTextFont; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 5; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Size6x10: + // Size 6x10 is only valid for text fonts, + // unless it's a VT240 in 132-column mode. + valid = _isTextFont || _columnsPerPage == 132; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 6; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Size7x10: + // Size 7x10 is only valid for text fonts. + valid = _isTextFont; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 7; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Default: + default: + // If we aren't given one of the predefined matrix sizes, then the + // matrix parameter is a pixel width, and height is obtained from the + // height parameter. This also applies for the default of 0, since a + // 0 width is treated as unknown (we'll try and estimate the expected + // width), and the height parameter can still give us the height. + _sizeDeclaredAsMatrix = false; + _declaredWidth = static_cast(cellMatrix); + _declaredHeight = cellHeight.value_or(0); + valid = (_declaredWidth <= MAX_WIDTH && _declaredHeight <= MAX_HEIGHT); + break; + } + } + + // Save the pending attributes, but don't update the current values until we + // are sure we have a valid sequence that can replace the current buffer. + _pendingCellMatrix = cellMatrix; + _pendingCellHeight = cellHeight.value_or(0); + _pendingFontSet = fontSet; + _pendingFontUsage = fontUsage; + + // Reset the used dimensions. These values will be determined by the extent + // of the sixel data that we receive in the following string sequence. + _usedWidth = 0; + _usedHeight = 0; + + return valid; +} + +bool FontBuffer::SetStartChar(const VTParameter startChar, + const DispatchTypes::DrcsCharsetSize charsetSize) noexcept +{ + switch (charsetSize) + { + case DispatchTypes::DrcsCharsetSize::Size94: + _startChar = startChar.value_or(1); + break; + case DispatchTypes::DrcsCharsetSize::Size96: + _startChar = startChar.value_or(0); + break; + default: + return false; + } + + _currentChar = _startChar; + _pendingCharsetSize = charsetSize; + _charsetIdInitialized = false; + _charsetIdBuilder.Clear(); + + return true; +} + +void FontBuffer::AddSixelData(const wchar_t ch) +{ + if (!_charsetIdInitialized) + { + _buildCharsetId(ch); + } + else if (ch >= L'?' && ch <= L'~') + { + _addSixelValue(ch - L'?'); + } + else if (ch == L'/') + { + _endOfSixelLine(); + } + else if (ch == L';') + { + _endOfCharacter(); + } +} + +bool FontBuffer::FinalizeSixelData() +{ + // If the charset ID hasn't been initialized this isn't a valid update. + RETURN_BOOL_IF_FALSE(_charsetIdInitialized); + + // Flush the current line to make sure we take all the used positions + // into account when calculating the font dimensions. + _endOfSixelLine(); + + // If the buffer has been cleared, we'll need to recalculate the dimensions + // using the latest attributes, adjust the character bit patterns to fit + // their true size, and fill in unused buffer positions with an error glyph. + if (_bufferCleared) + { + std::tie(_fullWidth, _fullHeight, _textWidth) = _calculateDimensions(); + _packAndCenterBitPatterns(); + _fillUnusedCharacters(); + } + + return true; +} + +gsl::span FontBuffer::GetBitPattern() const noexcept +{ + return { _buffer.data(), MAX_CHARS * _fullHeight }; +} + +til::size FontBuffer::GetCellSize() const +{ + return { _fullWidth, _fullHeight }; +} + +size_t FontBuffer::GetTextCenteringHint() const noexcept +{ + return _textCenteringHint; +} + +VTID FontBuffer::GetDesignation() const noexcept +{ + return _charsetId; +} + +void FontBuffer::_buildCharsetId(const wchar_t ch) +{ + // Note that we ignore any characters that are not valid in this state. + if (ch >= 0x20 && ch <= 0x2F) + { + _charsetIdBuilder.AddIntermediate(ch); + } + else if (ch >= 0x30 && ch <= 0x7E) + { + _pendingCharsetId = _charsetIdBuilder.Finalize(ch); + _charsetIdInitialized = true; + _prepareCharacterBuffer(); + } +} + +void FontBuffer::_prepareCharacterBuffer() +{ + // If any of the attributes have changed since the last time characters + // were downloaded, the font dimensions will need to be recalculated, and + // the buffer will need to be cleared. Otherwise we'll just be adding to + // the existing font, assuming the current dimensions. + if (_cellMatrix != _pendingCellMatrix || + _cellHeight != _pendingCellHeight || + _fontSet != _pendingFontSet || + _fontUsage != _pendingFontUsage || + _charsetSize != _pendingCharsetSize || + _charsetId != _pendingCharsetId) + { + // Replace the current attributes with the pending values. + _cellMatrix = _pendingCellMatrix; + _cellHeight = _pendingCellHeight; + _fontSet = _pendingFontSet; + _fontUsage = _pendingFontUsage; + _charsetSize = _pendingCharsetSize; + _charsetId = _pendingCharsetId; + + // Reset the font dimensions to the maximum supported size, since we + // can't be certain of the intended size until we've received all of + // the sixel data. These values will be recalculated once we can work + // out the terminal type that the font was originally designed for. + _fullWidth = MAX_WIDTH; + _fullHeight = MAX_HEIGHT; + _textWidth = MAX_WIDTH; + _textOffset = 0; + + // Clear the buffer. + _buffer.fill(0); + _bufferCleared = true; + } + else + { + _bufferCleared = false; + } + + _prepareNextCharacter(); +} + +void FontBuffer::_prepareNextCharacter() +{ + _lastChar = _currentChar; + _currentCharBuffer = std::next(_buffer.begin(), _currentChar * _fullHeight); + _sixelColumn = 0; + _sixelRow = 0; + + // If the buffer hasn't been cleared, we'll need to clear each character + // position individually, before adding any new sixel data. + if (!_bufferCleared && _currentChar < MAX_CHARS) + { + std::fill_n(_currentCharBuffer, _fullHeight, uint16_t{ 0 }); + } +} + +void FontBuffer::_addSixelValue(const size_t value) noexcept +{ + if (_currentChar < MAX_CHARS && _sixelColumn < _textWidth) + { + // Each sixel updates six pixels of a single column, so we setup a bit + // mask for the column we want to update, and then set that bit in each + // row for which there is a corresponding "on" bit in the input value. + const auto outputColumnBit = (0x8000 >> (_sixelColumn + _textOffset)); + auto outputIterator = _currentCharBuffer; + auto inputValueMask = 1; + for (size_t i = 0; i < 6 && _sixelRow + i < _fullHeight; i++) + { + *outputIterator |= (value & inputValueMask) ? outputColumnBit : 0; + outputIterator++; + inputValueMask <<= 1; + } + } + _sixelColumn++; +} + +void FontBuffer::_endOfSixelLine() +{ + // Move down six rows to the get to the next sixel position. + std::advance(_currentCharBuffer, 6); + _sixelRow += 6; + + // Keep track of the maximum width and height covered by the sixel data. + _usedWidth = std::max(_usedWidth, _sixelColumn); + _usedHeight = std::max(_usedHeight, _sixelRow); + + // Reset the column number to the start of the next line. + _sixelColumn = 0; +} + +void FontBuffer::_endOfCharacter() +{ + _endOfSixelLine(); + _currentChar++; + _prepareNextCharacter(); +} + +std::tuple FontBuffer::_calculateDimensions() const +{ + // If the size is declared as a matrix, this is most likely a VT2xx font, + // typically with a cell size of 10x10. However, in 132-column mode, the + // VT240 has a cell size of 6x10, but that's only for widths of 6 or less. + if (_sizeDeclaredAsMatrix) + { + if (_columnsPerPage == 132 && _declaredWidth <= 6) + { + // 6x10 cell with no clipping. + return { 6, 10, 0 }; + } + else + { + // 10x10 cell with text clipped to 8 pixels. + return { 10, 10, 8 }; + } + } + + // If we've been given explicit dimensions, and this is not a text font, + // then we assume those dimensions are the exact cell size. + if (_declaredWidth && _declaredHeight && !_isTextFont) + { + // Since this is not a text font, no clipping is required. + return { _declaredWidth, _declaredHeight, 0 }; + } + + // For most of the cases that follow, a text font will be clipped within + // the bounds of the declared width (if given). There are only a few cases + // where we'll need to use a hard-coded text width, and that's when the + // font appears to be targeting a VT2xx. + const auto textWidth = _isTextFont ? _declaredWidth : 0; + + // If the lines per page isn't 24, this must be targeting a VT420 or VT5xx. + // The cell width is 6 for 132 columns, and 10 for 80 columns. + // The cell height is 8 for 48 lines and 10 for 36 lines. + if (_linesPerPage != 24) + { + const auto cellWidth = _columnsPerPage == 132 ? 6 : 10; + const auto cellHeight = _linesPerPage == 48 ? 8 : 10; + return { cellWidth, cellHeight, textWidth }; + } + + // Now we're going to test whether the dimensions are in range for a number + // of known terminals. We use the declared dimensions if given, otherwise + // estimate the size from the used sixel values. If comparing a sixel-based + // height, though, we need to round up the target cell height to account for + // the fact that our used height will always be a multiple of six. + const auto inRange = [=](const size_t cellWidth, const size_t cellHeight) { + const auto sixelHeight = (cellHeight + 5) / 6 * 6; + const auto heightInRange = _declaredHeight ? _declaredHeight <= cellHeight : _usedHeight <= sixelHeight; + const auto widthInRange = _declaredWidth ? _declaredWidth <= cellWidth : _usedWidth <= cellWidth; + return heightInRange && widthInRange; + }; + + // In the case of a VT2xx font, you could only use a matrix size (which + // we've dealt with above), or a default size, so the tests below are only + // applicable for a VT2xx when no explicit dimensions have been declared. + const auto noDeclaredSize = _declaredWidth == 0 && _declaredHeight == 0; + + if (_columnsPerPage == 80) + { + if (inRange(8, 10) && noDeclaredSize) + { + // VT2xx - 10x10 cell with text clipped to 8 pixels. + return { 10, 10, 8 }; + } + else if (inRange(15, 12)) + { + // VT320 - 15x12 cell with default text width. + return { 15, 12, textWidth }; + } + else if (inRange(10, 16)) + { + // VT420 & VT5xx - 10x16 cell with default text width. + return { 10, 16, textWidth }; + } + else if (inRange(10, 20)) + { + // VT340 - 10x20 cell with default text width. + return { 10, 20, textWidth }; + } + else if (inRange(12, 30)) + { + // VT382 - 12x30 cell with default text width. + return { 12, 30, textWidth }; + } + else + { + // If all else fails, assume the maximum size. + return { MAX_WIDTH, MAX_HEIGHT, textWidth }; + } + } + else + { + if (inRange(6, 10) && noDeclaredSize) + { + // VT240 - 6x10 cell with no clipping. + return { 6, 10, 0 }; + } + else if (inRange(9, 12)) + { + // VT320 - 9x12 cell with default text width. + return { 9, 12, textWidth }; + } + else if (inRange(6, 16)) + { + // VT420 & VT5xx - 6x16 cell with default text width. + return { 6, 16, textWidth }; + } + else if (inRange(6, 20)) + { + // VT340 - 6x20 cell with default text width. + return { 6, 20, textWidth }; + } + else if (inRange(7, 30)) + { + // VT382 - 7x30 cell with default text width. + return { 7, 30, textWidth }; + } + else + { + // If all else fails, assume the maximum size. + return { MAX_WIDTH, MAX_HEIGHT, textWidth }; + } + } +} + +void FontBuffer::_packAndCenterBitPatterns() +{ + // If this is a text font, we'll clip the bits up to the text width and + // center them within the full cell width. For a full cell font we'll just + // use all of the bits, and no offset will be required. + _textWidth = _textWidth ? _textWidth : _fullWidth; + _textWidth = std::min(_textWidth, _fullWidth); + _textOffset = (_fullWidth - _textWidth) / 2; + const auto textClippingMask = ~(0xFFFF >> _textWidth); + + // If the text is given an explicit width, we check to what extent the + // content is offset from center. Knowing that information will enable the + // renderer to scale the font more symmetrically. + _textCenteringHint = _declaredWidth ? _fullWidth - (_declaredWidth + _textOffset * 2) : 0; + + // Initially the characters are written to the buffer assuming the maximum + // cell height, but now that we know the true height, we need to pack the + // buffer data so that each character occupies the exact number of scanlines + // that are required. + for (auto srcLine = 0u, dstLine = 0u; srcLine < _buffer.size(); srcLine++) + { + if ((srcLine % MAX_HEIGHT) < _fullHeight) + { + auto characterScanline = til::at(_buffer, srcLine); + characterScanline &= textClippingMask; + characterScanline >>= _textOffset; + til::at(_buffer, dstLine++) = characterScanline; + } + } +} + +void FontBuffer::_fillUnusedCharacters() +{ + // Every character in the buffer that hasn't been uploaded will be replaced + // with an error glyph (a reverse question mark). This includes every + // character prior to the start char, or after the last char. + const auto errorPattern = _generateErrorGlyph(); + for (auto ch = 0u; ch < MAX_CHARS; ch++) + { + if (ch < _startChar || ch > _lastChar) + { + auto charBuffer = std::next(_buffer.begin(), ch * _fullHeight); + std::copy_n(errorPattern.begin(), _fullHeight, charBuffer); + } + } +} + +std::array FontBuffer::_generateErrorGlyph() +{ + // We start with a bit pattern for a reverse question mark covering the + // maximum font resolution that we might need. + constexpr std::array inputBitPattern = { + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b001111111110000, + 0b011111111111000, + 0b111000000011100, + 0b111000000011100, + 0b111000000000000, + 0b111000000000000, + 0b111100000000000, + 0b011111000000000, + 0b000011110000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + 0b000000000000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + }; + + // Then for each possible width and height, we have hard-coded bit masks + // indicating a range of columns and rows to select from the base bitmap + // to produce a scaled down version of reasonable quality. + constexpr std::array widthMasks = { + // clang-format off + 0, 1, 3, 32771, 8457, 9481, 9545, 9673, 42441, 26061, + 58829, 28141, 60909, 63453, 63485, 65533, 65535 + // clang-format on + }; + constexpr std::array heightMasks = { + // clang-format off + 0, 1, 3, 7, 15, 1613952, 10002560, 10002816, 10068352, 10068353, + 26845569, 26847617, 26847619, 26864003, 28961155, 28961219, + 28961731, 62516163, 62516167, 129625031, 129625039, 129756111, + 263973839, 263974863, 268169167, 536604623, 536608719, 536608735, + 536870879, 1073741791, 2147483615, 2147483647, 4294967295 + // clang-format on + }; + + const auto widthMask = widthMasks.at(_fullWidth); + const auto heightMask = heightMasks.at(_fullHeight); + + auto outputBitPattern = std::array{}; + auto outputIterator = outputBitPattern.begin(); + for (auto y = 0; y < MAX_HEIGHT; y++) + { + const auto yBit = (1 << y); + if (heightMask & yBit) + { + const uint16_t inputScanline = til::at(inputBitPattern, y); + uint16_t outputScanline = 0; + for (auto x = MAX_WIDTH; x-- > 0;) + { + const auto xBit = 1 << x; + if (widthMask & xBit) + { + outputScanline <<= 1; + outputScanline |= (inputScanline & xBit) ? 1 : 0; + } + } + outputScanline <<= (MAX_WIDTH - _fullWidth); + *(outputIterator++) = outputScanline; + } + } + return outputBitPattern; +} diff --git a/src/terminal/adapter/FontBuffer.hpp b/src/terminal/adapter/FontBuffer.hpp new file mode 100644 index 000000000..bdd6fff72 --- /dev/null +++ b/src/terminal/adapter/FontBuffer.hpp @@ -0,0 +1,95 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontBuffer.hpp + +Abstract: +- This manages the construction and storage of font definitions for the VT DECDLD control sequence. +--*/ + +#pragma once + +#include "DispatchTypes.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class FontBuffer + { + public: + FontBuffer() noexcept; + ~FontBuffer() = default; + bool SetEraseControl(const DispatchTypes::DrcsEraseControl eraseControl) noexcept; + bool SetAttributes(const DispatchTypes::DrcsCellMatrix cellMatrix, + const VTParameter cellHeight, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage) noexcept; + bool SetStartChar(const VTParameter startChar, + const DispatchTypes::DrcsCharsetSize charsetSize) noexcept; + void AddSixelData(const wchar_t ch); + bool FinalizeSixelData(); + + gsl::span GetBitPattern() const noexcept; + til::size GetCellSize() const; + size_t GetTextCenteringHint() const noexcept; + VTID GetDesignation() const noexcept; + + private: + static constexpr size_t MAX_WIDTH = 16; + static constexpr size_t MAX_HEIGHT = 32; + static constexpr size_t MAX_CHARS = 96; + + void _buildCharsetId(const wchar_t ch); + void _prepareCharacterBuffer(); + void _prepareNextCharacter(); + void _addSixelValue(const size_t value) noexcept; + void _endOfSixelLine(); + void _endOfCharacter(); + + std::tuple _calculateDimensions() const; + void _packAndCenterBitPatterns(); + void _fillUnusedCharacters(); + std::array _generateErrorGlyph(); + + DispatchTypes::DrcsCellMatrix _cellMatrix; + DispatchTypes::DrcsCellMatrix _pendingCellMatrix; + size_t _cellHeight; + size_t _pendingCellHeight; + bool _sizeDeclaredAsMatrix; + size_t _declaredWidth; + size_t _declaredHeight; + size_t _usedWidth; + size_t _usedHeight; + size_t _fullWidth; + size_t _fullHeight; + size_t _textWidth; + size_t _textOffset; + size_t _textCenteringHint; + + DispatchTypes::DrcsFontSet _fontSet; + DispatchTypes::DrcsFontSet _pendingFontSet; + DispatchTypes::DrcsFontUsage _fontUsage; + DispatchTypes::DrcsFontUsage _pendingFontUsage; + size_t _linesPerPage; + size_t _columnsPerPage; + bool _isTextFont; + + DispatchTypes::DrcsCharsetSize _charsetSize; + DispatchTypes::DrcsCharsetSize _pendingCharsetSize; + VTID _charsetId{ 0 }; + VTID _pendingCharsetId{ 0 }; + bool _charsetIdInitialized; + VTIDBuilder _charsetIdBuilder; + size_t _startChar; + size_t _lastChar; + size_t _currentChar; + + using buffer_type = std::array; + buffer_type _buffer; + buffer_type::iterator _currentCharBuffer; + bool _bufferCleared; + size_t _sixelColumn; + size_t _sixelRow; + }; +} diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index fe53e49d8..b0efd81d8 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -23,6 +23,8 @@ namespace Microsoft::Console::VirtualTerminal class Microsoft::Console::VirtualTerminal::ITermDispatch { public: + using StringHandler = std::function; + #pragma warning(push) #pragma warning(disable : 26432) // suppress rule of 5 violation on interface because tampering with this is fraught with peril virtual ~ITermDispatch() = 0; @@ -130,6 +132,15 @@ public: virtual bool EndHyperlink() = 0; virtual bool DoConEmuAction(const std::wstring_view string) = 0; + + virtual StringHandler DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD }; inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() {} #pragma warning(pop) diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 4bd30cbc8..50db520c0 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -1884,6 +1884,7 @@ bool AdaptDispatch::SoftReset() // - Performs a communications line disconnect. // - Clears UDKs. // - Clears a down-line-loaded character set. +// * The soft font is reset in the renderer and the font buffer is deleted. // - Clears the screen. // * This is like Erase in Display (3), also clearing scrollback, as well as ED(2) // - Returns the cursor to the upper-left corner of the screen. @@ -1929,6 +1930,10 @@ bool AdaptDispatch::HardReset() // Delete all current tab stops and reapply _ResetTabStops(); + // Clear the soft font in the renderer and delete the font buffer. + success = _pConApi->PrivateUpdateSoftFont({}, {}, false) && success; + _fontBuffer = nullptr; + // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this RIS sequence to the connected // terminal application. We've reset our state, but the connected terminal @@ -2440,6 +2445,88 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view /*string*/) noexcept return false; } +// Method Description: +// - DECDLD - Downloads one or more characters of a dynamically redefinable +// character set (DRCS) with a specified pixel pattern. The pixel array is +// transmitted in sixel format via the returned StringHandler function. +// Arguments: +// - fontNumber - The buffer number into which the font will be loaded. +// - startChar - The first character in the set that will be replaced. +// - eraseControl - Which characters to erase before loading the new data. +// - cellMatrix - The character cell width (sometimes also height in legacy formats). +// - fontSet - The screen size for which the font is designed. +// - fontUsage - Whether it is a text font or a full-cell font. +// - cellHeight - The character cell height (if not defined by cellMatrix). +// - charsetSize - Whether the character set is 94 or 96 characters. +// Return Value: +// - a function to receive the pixel data or nullptr if parameters are invalid +ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) +{ + // If we're a conpty, we're just going to ignore the operation for now. + // There's no point in trying to pass it through without also being able + // to pass through the character set designations. + if (_pConApi->IsConsolePty()) + { + return nullptr; + } + + // The font buffer is created on demand. + if (!_fontBuffer) + { + _fontBuffer = std::make_unique(); + } + + // Only one font buffer is supported, so only 0 (default) and 1 are valid. + auto success = fontNumber <= 1; + success = success && _fontBuffer->SetEraseControl(eraseControl); + success = success && _fontBuffer->SetAttributes(cellMatrix, cellHeight, fontSet, fontUsage); + success = success && _fontBuffer->SetStartChar(startChar, charsetSize); + + // If any of the parameters are invalid, we return a null handler to let + // the state machine know we want to ignore the subsequent data string. + if (!success) + { + return nullptr; + } + + return [=](const auto ch) { + // We pass the data string straight through to the font buffer class + // until we receive an ESC, indicating the end of the string. At that + // point we can finalize the buffer, and if valid, update the renderer + // with the constructed bit pattern. + if (ch != AsciiChars::ESC) + { + _fontBuffer->AddSixelData(ch); + } + else if (_fontBuffer->FinalizeSixelData()) + { + // We also need to inform the character set mapper of the ID that + // will map to this font (we only support one font buffer so there + // will only ever be one active dynamic character set). + if (charsetSize == DispatchTypes::DrcsCharsetSize::Size96) + { + _termOutput.SetDrcs96Designation(_fontBuffer->GetDesignation()); + } + else + { + _termOutput.SetDrcs94Designation(_fontBuffer->GetDesignation()); + } + const auto bitPattern = _fontBuffer->GetBitPattern(); + const auto cellSize = _fontBuffer->GetCellSize(); + const auto centeringHint = _fontBuffer->GetTextCenteringHint(); + _pConApi->PrivateUpdateSoftFont(bitPattern, cellSize, centeringHint); + } + return true; + }; +} + // Routine Description: // - Determines whether we should pass any sequence that manipulates // TerminalInput's input generator through the PTY. It encapsulates diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 5ea582343..7eeade5cb 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -18,6 +18,7 @@ Author(s): #include "DispatchCommon.hpp" #include "conGetSet.hpp" #include "adaptDefaults.hpp" +#include "FontBuffer.hpp" #include "terminalOutput.hpp" #include "..\..\types\inc\sgrStack.hpp" @@ -130,6 +131,15 @@ namespace Microsoft::Console::VirtualTerminal bool DoConEmuAction(const std::wstring_view string) noexcept override; + StringHandler DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD + private: enum class ScrollDirection { @@ -187,6 +197,7 @@ namespace Microsoft::Console::VirtualTerminal std::unique_ptr _pConApi; std::unique_ptr _pDefaults; TerminalOutput _termOutput; + std::unique_ptr _fontBuffer; std::optional _initialCodePage; // We have two instances of the saved cursor state, because we need diff --git a/src/terminal/adapter/charsets.hpp b/src/terminal/adapter/charsets.hpp index 4ead5f14b..0a3a649ff 100644 --- a/src/terminal/adapter/charsets.hpp +++ b/src/terminal/adapter/charsets.hpp @@ -43,6 +43,11 @@ namespace Microsoft::Console::VirtualTerminal return rhs == lhs; } + // Note that the 94-character sets are deliberately defined with a size of + // 95 to avoid having to test the lower bound. We just alway leave the first + // entry - which is not meant to be mapped - as a SPACE or NBSP, which is at + // least visually equivalent to leaving it untranslated. + typedef CharSet AsciiBasedCharSet; typedef CharSet Latin1BasedCharSet94; typedef CharSet Latin1BasedCharSet96; @@ -1051,5 +1056,11 @@ namespace Microsoft::Console::VirtualTerminal { L'\x7e', L'\u00fc' }, // Latin Small Letter U With Diaeresis }; + // We're reserving 96 characters (U+EF20 to U+EF7F) from the Unicode + // Private Use Area for our dynamically redefinable characters sets. + static constexpr auto DRCS_BASE_CHAR = L'\uEF20'; + static constexpr auto Drcs94 = CharSet{ { DRCS_BASE_CHAR, '\x20' } }; + static constexpr auto Drcs96 = CharSet{}; + #pragma warning(pop) } diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index d604d8e06..e69ab753f 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -107,5 +107,9 @@ namespace Microsoft::Console::VirtualTerminal virtual bool PrivateAddHyperlink(const std::wstring_view uri, const std::wstring_view params) const = 0; virtual bool PrivateEndHyperlink() const = 0; + + virtual bool PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) = 0; }; } diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index eeb6f4574..1626e7a7d 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -12,6 +12,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 4675a7e3a..fb644781d 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -39,6 +39,9 @@ Source Files + + Source Files + @@ -83,6 +86,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index 0fadc810f..8b3ae4f43 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -32,6 +32,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\adaptDispatch.cpp \ ..\DispatchCommon.cpp \ + ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index d82a28c22..1fcb83a4e 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -123,4 +123,13 @@ public: bool EndHyperlink() noexcept override { return false; } bool DoConEmuAction(const std::wstring_view /*string*/) noexcept override { return false; } + + StringHandler DownloadDRCS(const size_t /*fontNumber*/, + const VTParameter /*startChar*/, + const DispatchTypes::DrcsEraseControl /*eraseControl*/, + const DispatchTypes::DrcsCellMatrix /*cellMatrix*/, + const DispatchTypes::DrcsFontSet /*fontSet*/, + const DispatchTypes::DrcsFontUsage /*fontUsage*/, + const VTParameter /*cellHeight*/, + const DispatchTypes::DrcsCharsetSize /*charsetSize*/) noexcept override { return nullptr; } }; diff --git a/src/terminal/adapter/terminalOutput.cpp b/src/terminal/adapter/terminalOutput.cpp index 14de84e13..944ff88b7 100644 --- a/src/terminal/adapter/terminalOutput.cpp +++ b/src/terminal/adapter/terminalOutput.cpp @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include -#include +#include "precomp.h" #include "charsets.hpp" #include "terminalOutput.hpp" #include "strsafe.h" @@ -19,91 +18,30 @@ TerminalOutput::TerminalOutput() noexcept bool TerminalOutput::Designate94Charset(size_t gsetNumber, const VTID charset) { - switch (charset) - { - case VTID("B"): // US ASCII - case VTID("1"): // Alternate Character ROM - return _SetTranslationTable(gsetNumber, Ascii); - case VTID("0"): // DEC Special Graphics - case VTID("2"): // Alternate Character ROM Special Graphics - return _SetTranslationTable(gsetNumber, DecSpecialGraphics); - case VTID("<"): // DEC Supplemental - return _SetTranslationTable(gsetNumber, DecSupplemental); - case VTID("A"): // British NRCS - return _SetTranslationTable(gsetNumber, BritishNrcs); - case VTID("4"): // Dutch NRCS - return _SetTranslationTable(gsetNumber, DutchNrcs); - case VTID("5"): // Finnish NRCS - case VTID("C"): // (fallback) - return _SetTranslationTable(gsetNumber, FinnishNrcs); - case VTID("R"): // French NRCS - return _SetTranslationTable(gsetNumber, FrenchNrcs); - case VTID("f"): // French NRCS (ISO update) - return _SetTranslationTable(gsetNumber, FrenchNrcsIso); - case VTID("9"): // French Canadian NRCS - case VTID("Q"): // (fallback) - return _SetTranslationTable(gsetNumber, FrenchCanadianNrcs); - case VTID("K"): // German NRCS - return _SetTranslationTable(gsetNumber, GermanNrcs); - case VTID("Y"): // Italian NRCS - return _SetTranslationTable(gsetNumber, ItalianNrcs); - case VTID("6"): // Norwegian/Danish NRCS - case VTID("E"): // (fallback) - return _SetTranslationTable(gsetNumber, NorwegianDanishNrcs); - case VTID("`"): // Norwegian/Danish NRCS (ISO standard) - return _SetTranslationTable(gsetNumber, NorwegianDanishNrcsIso); - case VTID("Z"): // Spanish NRCS - return _SetTranslationTable(gsetNumber, SpanishNrcs); - case VTID("7"): // Swedish NRCS - case VTID("H"): // (fallback) - return _SetTranslationTable(gsetNumber, SwedishNrcs); - case VTID("="): // Swiss NRCS - return _SetTranslationTable(gsetNumber, SwissNrcs); - case VTID("&4"): // DEC Cyrillic - return _SetTranslationTable(gsetNumber, DecCyrillic); - case VTID("&5"): // Russian NRCS - return _SetTranslationTable(gsetNumber, RussianNrcs); - case VTID("\"?"): // DEC Greek - return _SetTranslationTable(gsetNumber, DecGreek); - case VTID("\">"): // Greek NRCS - return _SetTranslationTable(gsetNumber, GreekNrcs); - case VTID("\"4"): // DEC Hebrew - return _SetTranslationTable(gsetNumber, DecHebrew); - case VTID("%="): // Hebrew NRCS - return _SetTranslationTable(gsetNumber, HebrewNrcs); - case VTID("%0"): // DEC Turkish - return _SetTranslationTable(gsetNumber, DecTurkish); - case VTID("%2"): // Turkish NRCS - return _SetTranslationTable(gsetNumber, TurkishNrcs); - case VTID("%5"): // DEC Supplemental - return _SetTranslationTable(gsetNumber, DecSupplemental); - case VTID("%6"): // Portuguese NRCS - return _SetTranslationTable(gsetNumber, PortugueseNrcs); - default: - return false; - } + const auto translationTable = _LookupTranslationTable94(charset); + RETURN_BOOL_IF_FALSE(!translationTable.empty()); + return _SetTranslationTable(gsetNumber, translationTable); } bool TerminalOutput::Designate96Charset(size_t gsetNumber, const VTID charset) { - switch (charset) - { - case VTID("A"): // ISO Latin-1 Supplemental - case VTID("<"): // (UPSS when assigned to Latin-1) - return _SetTranslationTable(gsetNumber, Latin1); - case VTID("B"): // ISO Latin-2 Supplemental - return _SetTranslationTable(gsetNumber, Latin2); - case VTID("L"): // ISO Latin-Cyrillic Supplemental - return _SetTranslationTable(gsetNumber, LatinCyrillic); - case VTID("F"): // ISO Latin-Greek Supplemental - return _SetTranslationTable(gsetNumber, LatinGreek); - case VTID("H"): // ISO Latin-Hebrew Supplemental - return _SetTranslationTable(gsetNumber, LatinHebrew); - case VTID("M"): // ISO Latin-5 Supplemental - return _SetTranslationTable(gsetNumber, Latin5); - default: - return false; - } + const auto translationTable = _LookupTranslationTable96(charset); + RETURN_BOOL_IF_FALSE(!translationTable.empty()); + return _SetTranslationTable(gsetNumber, translationTable); +} + +void TerminalOutput::SetDrcs94Designation(const VTID charset) +{ + _ReplaceDrcsTable(_LookupTranslationTable94(charset), Drcs94); + _drcsId = charset; + _drcsTranslationTable = Drcs94; +} + +void TerminalOutput::SetDrcs96Designation(const VTID charset) +{ + _ReplaceDrcsTable(_LookupTranslationTable96(charset), Drcs96); + _drcsId = charset; + _drcsTranslationTable = Drcs96; } #pragma warning(suppress : 26440) // Suppress spurious "function can be declared noexcept" warning @@ -186,9 +124,139 @@ wchar_t TerminalOutput::TranslateKey(const wchar_t wch) const noexcept return wchFound; } +const std::wstring_view TerminalOutput::_LookupTranslationTable94(const VTID charset) const +{ + // Note that the DRCS set can be designated with either a 94 or 96 sequence, + // regardless of the actual size of the set. This isn't strictly correct, + // but there is existing software that depends on this behavior. + if (charset == _drcsId) + { + return _drcsTranslationTable; + } + switch (charset) + { + case VTID("B"): // US ASCII + case VTID("1"): // Alternate Character ROM + return Ascii; + case VTID("0"): // DEC Special Graphics + case VTID("2"): // Alternate Character ROM Special Graphics + return DecSpecialGraphics; + case VTID("<"): // DEC Supplemental + return DecSupplemental; + case VTID("A"): // British NRCS + return BritishNrcs; + case VTID("4"): // Dutch NRCS + return DutchNrcs; + case VTID("5"): // Finnish NRCS + case VTID("C"): // (fallback) + return FinnishNrcs; + case VTID("R"): // French NRCS + return FrenchNrcs; + case VTID("f"): // French NRCS (ISO update) + return FrenchNrcsIso; + case VTID("9"): // French Canadian NRCS + case VTID("Q"): // (fallback) + return FrenchCanadianNrcs; + case VTID("K"): // German NRCS + return GermanNrcs; + case VTID("Y"): // Italian NRCS + return ItalianNrcs; + case VTID("6"): // Norwegian/Danish NRCS + case VTID("E"): // (fallback) + return NorwegianDanishNrcs; + case VTID("`"): // Norwegian/Danish NRCS (ISO standard) + return NorwegianDanishNrcsIso; + case VTID("Z"): // Spanish NRCS + return SpanishNrcs; + case VTID("7"): // Swedish NRCS + case VTID("H"): // (fallback) + return SwedishNrcs; + case VTID("="): // Swiss NRCS + return SwissNrcs; + case VTID("&4"): // DEC Cyrillic + return DecCyrillic; + case VTID("&5"): // Russian NRCS + return RussianNrcs; + case VTID("\"?"): // DEC Greek + return DecGreek; + case VTID("\">"): // Greek NRCS + return GreekNrcs; + case VTID("\"4"): // DEC Hebrew + return DecHebrew; + case VTID("%="): // Hebrew NRCS + return HebrewNrcs; + case VTID("%0"): // DEC Turkish + return DecTurkish; + case VTID("%2"): // Turkish NRCS + return TurkishNrcs; + case VTID("%5"): // DEC Supplemental + return DecSupplemental; + case VTID("%6"): // Portuguese NRCS + return PortugueseNrcs; + default: + return {}; + } +} + +const std::wstring_view TerminalOutput::_LookupTranslationTable96(const VTID charset) const +{ + // Note that the DRCS set can be designated with either a 94 or 96 sequence, + // regardless of the actual size of the set. This isn't strictly correct, + // but there is existing software that depends on this behavior. + if (charset == _drcsId) + { + return _drcsTranslationTable; + } + switch (charset) + { + case VTID("A"): // ISO Latin-1 Supplemental + case VTID("<"): // (UPSS when assigned to Latin-1) + return Latin1; + case VTID("B"): // ISO Latin-2 Supplemental + return Latin2; + case VTID("L"): // ISO Latin-Cyrillic Supplemental + return LatinCyrillic; + case VTID("F"): // ISO Latin-Greek Supplemental + return LatinGreek; + case VTID("H"): // ISO Latin-Hebrew Supplemental + return LatinHebrew; + case VTID("M"): // ISO Latin-5 Supplemental + return Latin5; + default: + return {}; + } +} + bool TerminalOutput::_SetTranslationTable(const size_t gsetNumber, const std::wstring_view translationTable) { _gsetTranslationTables.at(gsetNumber) = translationTable; // We need to reapply the locking shifts in case the underlying G-sets have changed. return LockingShift(_glSetNumber) && LockingShiftRight(_grSetNumber); } + +void TerminalOutput::_ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable) +{ + if (newTable.data() != oldTable.data()) + { + for (size_t gsetNumber = 0; gsetNumber < 4; gsetNumber++) + { + // Get the current translation table for this G-set. + auto gsetTable = _gsetTranslationTables.at(gsetNumber); + // If it's already a DRCS, replace it with a default charset. + if (Drcs94 == gsetTable || Drcs96 == gsetTable) + { + gsetTable = gsetNumber < 2 ? (std::wstring_view)Ascii : (std::wstring_view)Latin1; + } + // If it matches the old table, replace it with the new table. + if (gsetTable.data() == oldTable.data()) + { + gsetTable = newTable; + } + // Update the G-set entry with the new translation table. + _gsetTranslationTables.at(gsetNumber) = gsetTable; + } + // Reapply the locking shifts in case the underlying G-sets have changed. + LockingShift(_glSetNumber); + LockingShiftRight(_grSetNumber); + } +} diff --git a/src/terminal/adapter/terminalOutput.hpp b/src/terminal/adapter/terminalOutput.hpp index e82e067a7..aabc12657 100644 --- a/src/terminal/adapter/terminalOutput.hpp +++ b/src/terminal/adapter/terminalOutput.hpp @@ -28,6 +28,8 @@ namespace Microsoft::Console::VirtualTerminal wchar_t TranslateKey(const wchar_t wch) const noexcept; bool Designate94Charset(const size_t gsetNumber, const VTID charset); bool Designate96Charset(const size_t gsetNumber, const VTID charset); + void SetDrcs94Designation(const VTID charset); + void SetDrcs96Designation(const VTID charset); bool LockingShift(const size_t gsetNumber); bool LockingShiftRight(const size_t gsetNumber); bool SingleShift(const size_t gsetNumber); @@ -35,7 +37,10 @@ namespace Microsoft::Console::VirtualTerminal void EnableGrTranslation(boolean enabled); private: + const std::wstring_view _LookupTranslationTable94(const VTID charset) const; + const std::wstring_view _LookupTranslationTable96(const VTID charset) const; bool _SetTranslationTable(const size_t gsetNumber, const std::wstring_view translationTable); + void _ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable); std::array _gsetTranslationTables; size_t _glSetNumber = 0; @@ -44,5 +49,7 @@ namespace Microsoft::Console::VirtualTerminal std::wstring_view _grTranslationTable; mutable std::wstring_view _ssTranslationTable; boolean _grTranslationEnabled = false; + VTID _drcsId = 0; + std::wstring_view _drcsTranslationTable; }; } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index ce120ea3b..13f9476ae 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -567,6 +567,19 @@ public: return TRUE; } + bool PrivateUpdateSoftFont(const gsl::span /*bitPattern*/, + const SIZE cellSize, + const size_t /*centeringHint*/) noexcept override + { + Log::Comment(L"PrivateUpdateSoftFont MOCK called..."); + + Log::Comment(NoThrowString().Format(L"Cell size: %dx%d", cellSize.cx, cellSize.cy)); + VERIFY_ARE_EQUAL(_expectedCellSize.cx, cellSize.cx); + VERIFY_ARE_EQUAL(_expectedCellSize.cy, cellSize.cy); + + return TRUE; + } + void PrepData() { PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter. @@ -810,6 +823,8 @@ public: bool _privateSetDefaultBackgroundResult = false; COLORREF _expectedDefaultBackgroundColorValue = INVALID_COLOR; + SIZE _expectedCellSize = {}; + private: HANDLE _hCon; }; @@ -2353,6 +2368,229 @@ public: VERIFY_IS_FALSE(_pDispatch.get()->SetColorTableEntry(15, testColor)); } + TEST_METHOD(SoftFontSizeDetection) + { + using CellMatrix = DispatchTypes::DrcsCellMatrix; + using FontSet = DispatchTypes::DrcsFontSet; + using FontUsage = DispatchTypes::DrcsFontUsage; + + const auto decdld = [=](const auto cmw, const auto cmh, const auto ss, const auto u, const std::wstring_view data = {}) { + const auto ec = DispatchTypes::DrcsEraseControl::AllChars; + const auto css = DispatchTypes::DrcsCharsetSize::Size94; + const auto cellMatrix = static_cast(cmw); + const auto stringHandler = _pDispatch.get()->DownloadDRCS(0, 0, ec, cellMatrix, ss, u, cmh, css); + if (stringHandler) + { + stringHandler(L'B'); // Charset identifier + for (auto ch : data) + { + stringHandler(ch); + } + stringHandler(L'\033'); // String terminator + } + return stringHandler != nullptr; + }; + + // Matrix sizes at 80x24 should always use a 10x10 cell size (VT2xx). + Log::Comment(L"Matrix 5x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size5x10, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 6x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 7x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 0, FontSet::Size80x24, FontUsage::Text)); + + // At 132x24 the cell size is typically 6x10 (VT240), but could be 10x10 (VT220) + Log::Comment(L"Matrix 5x10 for 132x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size5x10, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 6x10 for 132x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 7x10 for 132x24 font set with text usage (VT220 only)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 0, FontSet::Size132x24, FontUsage::Text)); + + // Full cell usage is invalid for all matrix sizes except 6x10 at 132x24. + Log::Comment(L"Matrix 5x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size5x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 6x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size6x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 7x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size7x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 5x10 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size5x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 6x10 for 132x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 7x10 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size7x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + + // Matrix size 1 is always invalid. + Log::Comment(L"Matrix 1 for 80x24 font set with text usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 1 for 132x24 font set with text usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 1 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 1 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size132x24, FontUsage::FullCell)); + + // The height parameter has no effect when a matrix size is used. + Log::Comment(L"Matrix 7x10 with unused height parameter"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 20, FontSet::Size80x24, FontUsage::Text)); + + // Full cell fonts with explicit dimensions are accepted as their given cell size. + Log::Comment(L"Explicit 13x17 for 80x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 13, 17 }; + VERIFY_IS_TRUE(decdld(13, 17, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Explicit 9x25 for 132x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 9, 25 }; + VERIFY_IS_TRUE(decdld(9, 25, FontSet::Size132x24, FontUsage::FullCell)); + + // Cell sizes outside the maximum supported range (16x32) are invalid. + Log::Comment(L"Explicit 18x38 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(18, 38, FontSet::Size80x24, FontUsage::FullCell)); + + // Text fonts with explicit dimensions are interpreted as their closest matching device. + Log::Comment(L"Explicit 12x12 for 80x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + VERIFY_IS_TRUE(decdld(12, 12, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 9x20 for 80x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + VERIFY_IS_TRUE(decdld(9, 20, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 10x30 for 80x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + VERIFY_IS_TRUE(decdld(10, 30, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 8x16 for 80x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + VERIFY_IS_TRUE(decdld(8, 16, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 7x12 for 132x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + VERIFY_IS_TRUE(decdld(7, 12, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 5x20 for 132x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + VERIFY_IS_TRUE(decdld(5, 20, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 6x30 for 132x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + VERIFY_IS_TRUE(decdld(6, 30, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 5x16 for 132x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + VERIFY_IS_TRUE(decdld(5, 16, FontSet::Size132x24, FontUsage::Text)); + + // Font sets with more than 24 lines must be VT420/VT5xx. + Log::Comment(L"80x36 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x36, FontUsage::Text)); + Log::Comment(L"80x48 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x48, FontUsage::Text)); + Log::Comment(L"132x36 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x36, FontUsage::Text)); + Log::Comment(L"132x48 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x48, FontUsage::Text)); + Log::Comment(L"80x36 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x36, FontUsage::FullCell)); + Log::Comment(L"80x48 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x48, FontUsage::FullCell)); + Log::Comment(L"132x36 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x36, FontUsage::FullCell)); + Log::Comment(L"132x48 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x48, FontUsage::FullCell)); + + // Without an explicit size, the cell size is estimated from the number of sixels + // used in the character bitmaps. But note that sixel heights are always a multiple + // of 6, so will often be larger than the cell size for which they were intended. + Log::Comment(L"8x12 bitmap for 80x24 font set with text usage (VT2xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + const auto bitmapOf8x12 = L"????????/????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf8x12)); + Log::Comment(L"12x12 bitmap for 80x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + const auto bitmapOf12x12 = L"????????????/????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf12x12)); + Log::Comment(L"9x24 bitmap for 80x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + const auto bitmapOf9x24 = L"?????????/?????????/?????????/?????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf9x24)); + Log::Comment(L"10x30 bitmap for 80x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + const auto bitmapOf10x30 = L"??????????/??????????/??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf10x30)); + Log::Comment(L"8x18 bitmap for 80x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + const auto bitmapOf8x18 = L"????????/????????/????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf8x18)); + + Log::Comment(L"5x12 bitmap for 132x24 font set with text usage (VT240)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + const auto bitmapOf5x12 = L"?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x12)); + Log::Comment(L"7x12 bitmap for 132x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + const auto bitmapOf7x12 = L"???????/???????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf7x12)); + Log::Comment(L"5x24 bitmap for 132x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + const auto bitmapOf5x24 = L"?????/?????/?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x24)); + Log::Comment(L"6x30 bitmap for 132x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + const auto bitmapOf6x30 = L"??????/??????/??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf6x30)); + Log::Comment(L"5x18 bitmap for 132x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + const auto bitmapOf5x18 = L"?????/?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x18)); + + Log::Comment(L"15x12 bitmap for 80x24 font set with full cell usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + const auto bitmapOf15x12 = L"???????????????/???????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf15x12)); + Log::Comment(L"10x24 bitmap for 80x24 font set with full cell usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + const auto bitmapOf10x24 = L"??????????/??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf10x24)); + Log::Comment(L"12x30 bitmap for 80x24 font set with full cell usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + const auto bitmapOf12x30 = L"????????????/????????????/????????????/????????????/????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf12x30)); + Log::Comment(L"10x18 bitmap for 80x24 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + const auto bitmapOf10x18 = L"??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf10x18)); + + Log::Comment(L"6x12 bitmap for 132x24 font set with full cell usage (VT240)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + const auto bitmapOf6x12 = L"??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x12)); + Log::Comment(L"9x12 bitmap for 132x24 font set with full cell usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + const auto bitmapOf9x12 = L"?????????/?????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf9x12)); + Log::Comment(L"6x24 bitmap for 132x24 font set with full cell usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + const auto bitmapOf6x24 = L"??????/??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x24)); + Log::Comment(L"7x30 bitmap for 132x24 font set with full cell usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + const auto bitmapOf7x30 = L"???????/???????/???????/???????/???????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf7x30)); + Log::Comment(L"6x18 bitmap for 132x24 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + const auto bitmapOf6x18 = L"??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x18)); + } + private: TestGetSet* _testGetSet; // non-ownership pointer std::unique_ptr _pDispatch; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 0ecd5ea4d..953fce415 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -645,10 +645,27 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete // - parameters - set of numeric parameters collected while parsing the sequence. // Return Value: // - the data string handler function or nullptr if the sequence is not supported -IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(const VTID /*id*/, const VTParameters /*parameters*/) noexcept +IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(const VTID id, const VTParameters parameters) { StringHandler handler = nullptr; + switch (id) + { + case DcsActionCodes::DECDLD_DownloadDRCS: + handler = _dispatch->DownloadDRCS(parameters.at(0), + parameters.at(1), + parameters.at(2), + parameters.at(3), + parameters.at(4), + parameters.at(5), + parameters.at(6), + parameters.at(7)); + break; + default: + handler = nullptr; + break; + } + _ClearLastChar(); return handler; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 1f79795a9..8ef831160 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -39,7 +39,7 @@ namespace Microsoft::Console::VirtualTerminal bool ActionCsiDispatch(const VTID id, const VTParameters parameters) override; - StringHandler ActionDcsDispatch(const VTID id, const VTParameters parameters) noexcept override; + StringHandler ActionDcsDispatch(const VTID id, const VTParameters parameters) override; bool ActionClear() noexcept override; @@ -144,6 +144,11 @@ namespace Microsoft::Console::VirtualTerminal DECSCPP_SetColumnsPerPage = VTID("$|"), }; + enum DcsActionCodes : uint64_t + { + DECDLD_DownloadDRCS = VTID("{"), + }; + enum Vt52ActionCodes : uint64_t { CursorUp = VTID("A"), diff --git a/src/til/ut_til/MathTests.cpp b/src/til/ut_til/MathTests.cpp index 056d03edc..6e031fb44 100644 --- a/src/til/ut_til/MathTests.cpp +++ b/src/til/ut_til/MathTests.cpp @@ -44,10 +44,7 @@ class MathTests _RunCases(til::math::truncating, cases); - const auto fn = []() { - const auto v = til::math::details::truncating_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::truncating_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(Ceiling) @@ -65,10 +62,7 @@ class MathTests _RunCases(til::math::ceiling, cases); - const auto fn = []() { - const auto v = til::math::details::ceiling_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::ceiling_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(Flooring) @@ -86,10 +80,7 @@ class MathTests _RunCases(til::math::flooring, cases); - const auto fn = []() { - const auto v = til::math::details::flooring_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::flooring_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(Rounding) @@ -107,10 +98,7 @@ class MathTests _RunCases(til::math::rounding, cases); - const auto fn = []() { - const auto v = til::math::details::rounding_t::cast(NAN); - }; - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + VERIFY_THROWS_SPECIFIC(til::math::details::rounding_t::cast(NAN), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); } TEST_METHOD(NormalIntegers) diff --git a/src/til/ut_til/PointTests.cpp b/src/til/ut_til/PointTests.cpp index 00a25b193..a98531bd1 100644 --- a/src/til/ut_til/PointTests.cpp +++ b/src/til/ut_til/PointTests.cpp @@ -765,7 +765,6 @@ class PointTests Log::Comment(L"4.) Division by zero"); { - constexpr ptrdiff_t bigSize = std::numeric_limits().max(); const til::point pt{ 1, 1 }; const int scale = 0; diff --git a/src/til/ut_til/string.cpp b/src/til/ut_til/string.cpp index eb4fbe488..c25a19972 100644 --- a/src/til/ut_til/string.cpp +++ b/src/til/ut_til/string.cpp @@ -53,6 +53,56 @@ class StringTests VERIFY_IS_TRUE(til::ends_with("0abc", "abc")); } + // Normally this would be the spot where you'd find a TEST_METHOD(from_wchars). + // I didn't quite trust my coding skills and thus opted to use fuzz-testing. + // The below function was used to test from_wchars for unsafety and conformance with clang's strtoul. + // The test was run as: + // clang++ -fsanitize=address,undefined,fuzzer -std=c++17 file.cpp + // and was run for 20min across 16 jobs in parallel. +#if 0 + extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) + { + while (size > 0 && (isspace(*data) || *data == '+' || *data == '-')) + { + --size; + ++data; + } + + if (size == 0 || size > 127) + { + return 0; + } + + char narrow_buffer[128]; + wchar_t wide_buffer[128]; + + memcpy(narrow_buffer, data, size); + for (size_t i = 0; i < size; ++i) + { + wide_buffer[i] = data[i]; + } + + // strtoul requires a null terminator + narrow_buffer[size] = 0; + wide_buffer[size] = 0; + + char* end; + const auto expected = strtoul(narrow_buffer, &end, 0); + if (end != narrow_buffer + size || expected >= ULONG_MAX / 16) + { + return 0; + } + + const auto actual = from_wchars({ wide_buffer, size }); + if (expected != actual) + { + __builtin_trap(); + } + + return 0; + } +#endif + TEST_METHOD(tolower_ascii) { for (wchar_t ch = 0; ch < 128; ++ch) diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 69f531c09..d6aac19cd 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -262,61 +262,73 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexc try { - const auto& buffer = _pData->GetTextBuffer(); - const auto bufferSize = _getBufferSize(); - const auto bufferEnd = bufferSize.EndExclusive(); - - if (unit == TextUnit_Character) - { - _start = buffer.GetGlyphStart(_start); - _end = buffer.GetGlyphEnd(_start); - } - else if (unit <= TextUnit_Word) - { - // expand to word - _start = buffer.GetWordStart(_start, _wordDelimiters, true); - _end = buffer.GetWordEnd(_start, _wordDelimiters, true); - - // GetWordEnd may return the actual end of the TextBuffer. - // If so, just set it to this value of bufferEnd - if (!bufferSize.IsInBounds(_end)) - { - _end = bufferEnd; - } - } - else if (unit <= TextUnit_Line) - { - if (_start == bufferEnd) - { - // Special case: if we are at the bufferEnd, - // move _start back one, instead of _end forward - _start.X = 0; - _start.Y = base::ClampSub(_start.Y, 1); - _end = bufferEnd; - } - else - { - // expand to line - _start.X = 0; - _end.X = 0; - _end.Y = base::ClampAdd(_start.Y, 1); - } - } - else - { - // TODO GH#6986: properly handle "end of buffer" as last character - // instead of last cell - // expand to document - _start = bufferSize.Origin(); - _end = bufferSize.EndExclusive(); - } - + _expandToEnclosingUnit(unit); UiaTracing::TextRange::ExpandToEnclosingUnit(unit, *this); return S_OK; } CATCH_RETURN(); } +// Method Description: +// - Moves _start and _end endpoints to encompass the enclosing text unit. +// (i.e. word --> enclosing word, line --> enclosing line) +// - IMPORTANT: this does _not_ lock the console +// Arguments: +// - attributeId - the UIA text attribute identifier we're expanding by +// Return Value: +// - +void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit) +{ + const auto& buffer = _pData->GetTextBuffer(); + const auto bufferSize = _getBufferSize(); + const auto bufferEnd = bufferSize.EndExclusive(); + + if (unit == TextUnit_Character) + { + _start = buffer.GetGlyphStart(_start); + _end = buffer.GetGlyphEnd(_start); + } + else if (unit <= TextUnit_Word) + { + // expand to word + _start = buffer.GetWordStart(_start, _wordDelimiters, true); + _end = buffer.GetWordEnd(_start, _wordDelimiters, true); + + // GetWordEnd may return the actual end of the TextBuffer. + // If so, just set it to this value of bufferEnd + if (!bufferSize.IsInBounds(_end)) + { + _end = bufferEnd; + } + } + else if (unit <= TextUnit_Line) + { + if (_start == bufferEnd) + { + // Special case: if we are at the bufferEnd, + // move _start back one, instead of _end forward + _start.X = 0; + _start.Y = base::ClampSub(_start.Y, 1); + _end = bufferEnd; + } + else + { + // expand to line + _start.X = 0; + _end.X = 0; + _end.Y = base::ClampAdd(_start.Y, 1); + } + } + else + { + // TODO GH#6986: properly handle "end of buffer" as last character + // instead of last cell + // expand to document + _start = bufferSize.Origin(); + _end = bufferSize.EndExclusive(); + } +} + // Method Description: // - Verify that the given attribute has the desired formatting saved in the attributeId and val // Arguments: @@ -926,7 +938,12 @@ try const auto maxLengthOpt = (maxLength == -1) ? std::nullopt : std::optional{ maxLength }; + _pData->LockConsole(); + auto Unlock = wil::scope_exit([this]() noexcept { + _pData->UnlockConsole(); + }); const auto text = _getTextValue(maxLengthOpt); + Unlock.reset(); *pRetVal = SysAllocString(text.c_str()); RETURN_HR_IF_NULL(E_OUTOFMEMORY, *pRetVal); @@ -946,11 +963,6 @@ CATCH_RETURN(); #pragma warning(disable : 26447) // compiler isn't filtering throws inside the try/catch std::wstring UiaTextRangeBase::_getTextValue(std::optional maxLength) const { - _pData->LockConsole(); - auto Unlock = wil::scope_exit([&]() noexcept { - _pData->UnlockConsole(); - }); - std::wstring textData{}; if (!IsDegenerate()) { @@ -994,6 +1006,7 @@ std::wstring UiaTextRangeBase::_getTextValue(std::optional maxLeng IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, _In_ int count, _Out_ int* pRetVal) noexcept +try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; @@ -1011,26 +1024,22 @@ IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, constexpr auto endpoint = TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start; constexpr auto preventBufferEnd = true; const auto wasDegenerate = IsDegenerate(); - try + if (unit == TextUnit::TextUnit_Character) { - if (unit == TextUnit::TextUnit_Character) - { - _moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBufferEnd); - } - else if (unit <= TextUnit::TextUnit_Word) - { - _moveEndpointByUnitWord(count, endpoint, pRetVal, preventBufferEnd); - } - else if (unit <= TextUnit::TextUnit_Line) - { - _moveEndpointByUnitLine(count, endpoint, pRetVal, preventBufferEnd); - } - else if (unit <= TextUnit::TextUnit_Document) - { - _moveEndpointByUnitDocument(count, endpoint, pRetVal, preventBufferEnd); - } + _moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Word) + { + _moveEndpointByUnitWord(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Line) + { + _moveEndpointByUnitLine(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Document) + { + _moveEndpointByUnitDocument(count, endpoint, pRetVal, preventBufferEnd); } - CATCH_RETURN(); // If we actually moved... if (*pRetVal != 0) @@ -1044,13 +1053,14 @@ IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, else { // then just expand to get our _end - ExpandToEnclosingUnit(unit); + _expandToEnclosingUnit(unit); } } UiaTracing::TextRange::Move(unit, count, *pRetVal, *this); return S_OK; } +CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint, _In_ TextUnit unit, diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index 47b9778a0..02bf0461b 100644 --- a/src/types/UiaTextRangeBase.hpp +++ b/src/types/UiaTextRangeBase.hpp @@ -153,6 +153,8 @@ namespace Microsoft::Console::Types void _getBoundingRect(const til::rectangle textRect, _Inout_ std::vector& coords) const; + void _expandToEnclosingUnit(TextUnit unit); + void _moveEndpointByUnitCharacter(_In_ const int moveCount, _In_ const TextPatternRangeEndpoint endpoint, diff --git a/tools/GenerateHeaderForJson.ps1 b/tools/GenerateHeaderForJson.ps1 index 1116275e0..953ca8594 100644 --- a/tools/GenerateHeaderForJson.ps1 +++ b/tools/GenerateHeaderForJson.ps1 @@ -2,34 +2,26 @@ # the contents of that json files as a constexpr string_view in the header. param ( - [parameter(Mandatory=$true, Position=0)] + [parameter(Mandatory = $true)] [string]$JsonFile, - [parameter(Mandatory=$true, Position=1)] + [parameter(Mandatory = $true)] [string]$OutPath, - [parameter(Mandatory=$true, Position=2)] + [parameter(Mandatory = $true)] [string]$VariableName ) -# Load the xml files. +$fullPath = Resolve-Path $JsonFile $jsonData = Get-Content $JsonFile -Write-Output "// Copyright (c) Microsoft Corporation" | Out-File -FilePath $OutPath -Encoding ASCII -Write-Output "// Licensed under the MIT license." | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "" | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "// THIS IS AN AUTO-GENERATED FILE" | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "// Generated from " | Out-File -FilePath $OutPath -Encoding ASCII -Append -NoNewline -$fullPath = Resolve-Path -Path $JsonFile -Write-Output $fullPath.Path | Out-File -FilePath $OutPath -Encoding ASCII -Append -Write-Output "constexpr std::string_view $($VariableName){ " | Out-File -FilePath $OutPath -Encoding ASCII -Append - -# Write each line escaped on its own, as it's own literal. This file is _very -# big_, so big that it cannot fit in a single string literal :O The compiler is, -# however, smart enough to just concatenate all these literals into one big -# string. -$jsonData | foreach { - Write-Output "R`"($_`n)`"" | Out-File -FilePath $OutPath -Encoding ASCII -Append -} -Write-Output "};" | Out-File -FilePath $OutPath -Encoding ASCII -Append - +@( + "// Copyright (c) Microsoft Corporation", + "// Licensed under the MIT license.", + "", + "// THIS IS AN AUTO-GENERATED FILE", + "// Generated from $($fullPath.Path)", + "constexpr std::string_view $($VariableName){", + ($jsonData | ForEach-Object { "R`"#($_`n)#`"" }), + "};" +) | Out-File -FilePath $OutPath -Encoding utf8 diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index 84b85c2dc..bad0afb0d 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -193,11 +193,9 @@ function Invoke-OpenConsoleTests() return } $OpenConsolePlatform = $Platform - $TestHostAppPath = "$root\bin\$OpenConsolePlatform\$Configuration\TestHostApp" if ($Platform -eq 'x86') { $OpenConsolePlatform = 'Win32' - $TestHostAppPath = "$root\$Configuration\TestHostApp" } $OpenConsolePath = "$env:OpenConsoleroot\bin\$OpenConsolePlatform\$Configuration\OpenConsole.exe" $TaefExePath = "$root\packages\Microsoft.Taef.10.60.210621002\build\Binaries\$Platform\te.exe" @@ -236,14 +234,7 @@ function Invoke-OpenConsoleTests() { if ($t.type -eq "unit") { - if ($t.runInHostApp -eq "true") - { - & $TaefExePath "$TestHostAppPath\$($t.binary)" $TaefArgs - } - else - { - & $TaefExePath "$BinDir\$($t.binary)" $TaefArgs - } + & $TaefExePath "$BinDir\$($t.binary)" $TaefArgs } elseif ($t.type -eq "ft") { diff --git a/tools/TestTableWriter/GenerateTests.ps1 b/tools/TestTableWriter/GenerateTests.ps1 new file mode 100644 index 000000000..c4432de5c --- /dev/null +++ b/tools/TestTableWriter/GenerateTests.ps1 @@ -0,0 +1,164 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +################################################################################ +# This script generates the an array of UiaTextRange tests suitable for replacing the body of +# src\interactivity\win32\ut_interactivity_win32\UiaTextRangeTests.cpp TEST_METHOD(GeneratedMovementTests) +# +# See tools\TestTableWriter\README.md for more details on how to use this script. + +[CmdletBinding()] +Param( + [Parameter(Position=0, ValueFromPipeline=$true)] + [string]$TestPath = "UiaTests.csv" +) + +# 0. Generate a comment telling people to not modify these tests in the .cpp +$result = "// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// DO NOT MODIFY THESE TESTS DIRECTLY +// These were generated by tools\TestTableWriter\GenerateTests.ps1 +// Read tools\TestTableWriter\README.md for more details" + +# TODO: THIS IS PROBABLY WRONG. Bottom/Right are exclusive (I think?) +# 1. Define a few helpful variables to make life easier. +$result += " +// Define a few helpful variables +constexpr til::rectangle bufferSize{ 0, 0, 80, 300 }; +constexpr short midX{ 40 }; +constexpr short midY{ 150 }; +constexpr short midPopulatedY{ 75 }; +constexpr til::point origin{ 0, 0 }; +constexpr til::point midTop{ midX, 0 }; +constexpr til::point midHistory{ midX, midPopulatedY }; +constexpr til::point midDocEnd{ midX, midY }; +constexpr til::point lastCharPos{ 79, midY }; +constexpr til::point docEnd{ 0, midY + 1 }; +constexpr til::point midEmptySpace{ midX, midY + midPopulatedY }; +constexpr til::point bufferEnd{ 79, 299 }; +constexpr til::point endExclusive{ 0, 300 };`n" + +# 2. Import the CSV test file and find all of the variables we need +$tests = Import-Csv $TestPath; +$vars = New-Object System.Collections.Generic.SortedSet[string]; +foreach ($test in $tests) +{ + $vars.Add($test.Start) > $null; + $vars.Add($test.End) > $null; + $vars.Add($test.Result_Start) > $null; + $vars.Add($test.Result_End) > $null; +} + +# 3. Define each of the vars +# 3.a. Some of the variables were already defined at the beginning. So let's remove those. +$vars.Remove("origin") > $null; +$vars.Remove("midTop") > $null; +$vars.Remove("midHistory") > $null; +$vars.Remove("docEnd") > $null; + +# 3.b. Now all of the remaining vars can be deduced from standard vars +foreach ($var in $vars) +{ + # Extract the standard var from the name + $standardVar = $var.Contains("Left") ? $var.Split("Left") : $var.Substring(0, $var.length - 3); + + # i. Contains number --> requires movement + if ($var -match ".*\d+.*") + { + # 3rd to last character denotes the movement direction + # P --> plus/forwards + # M --> minus/backwards + $moveForward = $var.substring($var.length - 3, 1) -eq 'P'; + + # 2nd to last character denotes the movement amount + $moveAmt = $var.substring($var.length -2, 1); + + # last character denotes the movement type + switch ($var.substring($var.length - 1, 1)) { + 'C' # move by character + { + if ($moveForward) + { + $result += "constexpr auto {0}{{ point_offset_by_char({1}, bufferSize, {2}) }};" -f $var, $standardVar, $moveAmt; + } + else + { + $result += "constexpr auto {0}{{ point_offset_by_char({1}, bufferSize, -{2}) }};" -f $var, $standardVar, $moveAmt; + } + } + 'L' # move by line + { + if ($moveForward) + { + $result += "constexpr auto {0}{{ point_offset_by_line({1}, bufferSize, {2}) }};" -f $var, $standardVar, $moveAmt; + } + else + { + $result += "constexpr auto {0}{{ point_offset_by_line({1}, bufferSize, -{2}) }};" -f $var, $standardVar, $moveAmt; + } + } + Default { Write-Host "Error: unknown variable movement type" -ForegroundColor Red } + } + } + # ii. Contains "Left" --> set X to left + elseif ($var.Contains("Left")) + { + $result += "constexpr til::point " + $var + "{ bufferSize.left(), " + $standardVar + ".y() };"; + } + $result += "`n"; +} + +# 4. Write the tests +# 4.a. Introduce a struct to store each test as +$result += "struct GeneratedMovementTestInput +{ + TextUnit unit; + int moveAmount; + til::point start; + til::point end; +}; +struct GeneratedMovementTestExpected +{ + int moveAmount; + til::point start; + til::point end; +}; +struct GeneratedMovementTest +{ + std::wstring_view name; + GeneratedMovementTestInput input; + GeneratedMovementTestExpected expected; + bool skip; +};`n`n"; + +# 4.b. Iterate through CSV file and generate a test for each one +$result += "static constexpr std::array s_movementTests`n{{`n" -f $tests.count; +foreach ($test in $tests) +{ + $degeneratePrefix = $test.degenerate -eq "TRUE" ? "" : "non-"; + $movementType = $test.TextUnit.substring(9); + $testName = "L`"Move {0}degenerate range at position {1} {2} times by {3}`"" -f $degeneratePrefix, $test.Position, $test.MoveAmount, $movementType; + $testInput = "GeneratedMovementTestInput{{ + TextUnit::{0}, + {1}, + {2}, + {3} + }}" -f $test.TextUnit, $test.MoveAmount, $test.Start, $test.End; + $testExpected = "GeneratedMovementTestExpected{{ + {0}, + {1}, + {2} + }}" -f $test.Result_MoveAmount, $test.Result_Start, $test.Result_End; + $skip = $test.Skip -eq "TRUE" ? "true" : "false"; + + $result += " GeneratedMovementTest{{ + {0}, + {1}, + {2}, + {3} + }},`n" -f $testName, $testInput, $testExpected, $skip; +} +$result += "};`n`n" + +$result > "..\..\src\interactivity\win32\ut_interactivity_win32\GeneratedUiaTextRangeMovementTests.g.cpp"; diff --git a/tools/TestTableWriter/README.md b/tools/TestTableWriter/README.md new file mode 100644 index 000000000..a708f1695 --- /dev/null +++ b/tools/TestTableWriter/README.md @@ -0,0 +1,90 @@ +--- +author: Carlos Zamora @carlos-zamora +created on: 2021-08-05 +last updated: 2021-08-05 +--- + +# Test Table Writer + +The Test Table Writer was written as a method to generate UI Automation tests for OpenConsole. UI Automation has a lot of corner cases, so we developed this workflow to simplify storing and updating these test cases (particularly revolving around movement). + +# How to use it +1. Update `UiaTests.csv`: + - This file is used to store the tests in a compact format. The defined columns include... + - Degenerate: is this a degenerate range? + - Position: see the position chart below + - TextUnit: what text unit to move by + - MoveAmount: how many times to move + - Start: the start endpoint of the text range. Represented by a variable name used to signify a position in the buffer. See the variable heuristics section below. + - End: the start endpoint of the text range. Represented by a variable name used to signify a position in the buffer. See the variable heuristics section below. + - Result_MoveAmount: the expected amount to have moved by + - Result_Start: the expected position of the start endpoint after executing the move operation + - Result_End: the expected position of the end endpoint after executing the move operation + - Skip: skip the test. Can be used for failing tests. + - Each row represents a new test in a compact format. + - Use the position chart and the variable heuristics below to add more tests easily. +2. Run `GenerateTests.ps1` + - `GenerateTests.ps1` will load `UiaTests.csv` and export the tests and any necessary variables to "src\interactivity\win32\ut_interactivity_win32\GeneratedUiaTextRangeMovementTests.g.cpp". +3. Build and run the tests + - Build UiaTextRangeTests + - Go to bin\x64\Debug + - Run `clear; .\TE.exe /name:*UiaTextRangeTests*GeneratedMovementTests* .\Conhost.Interactivity.Win32.Unit.Tests.dll` in PowerShell +5. If the tests pass, upload any changes to `UiaTests.csv` and `GeneratedUiaTextRangeMovementTests.g.cpp`. Be sure to run the code formatter. + +# Helpful tips +- How to verify a test is authored correctly + - use MS Word to generate some text (try typing "=lorem(5,5)" then pressing enter to generate text) + - use Accessibility Insights to run the UIA API on MS Word's ITextProvider + - if you create a selection (or move the cursor), then tell Accessibility Insights to get the "Selection" range (or refresh it), you can easily set up a test case +- Run the tests via Visual Studio + - In the Solution Explorer, right-click Interactivity.Win32.Tests.Unit and select "Set as Startup Project" + - right-click it again and select "Properties" + - In the Debugging section, set.. + - "Command" --> `$(OutDir)/TE.exe` + - "Command Arguments" --> `$(TargetPath) /name:*uiatextrange*generated* /inproc` + +# Position chart +The text buffer is assumed to be partially filled. Specifically, the top half of the text buffer contains text, and each row is filled with 9 segments of alternating text. For visualization, +the ascii diagram below shows what the text buffer may look like. +``` ++---------------------------+ +|1XX XXX X2X XXX XXX| +|XXX XXX XXX XXX XXX| +|XXX XXX X3X XXX XXX| +|XXX XXX XXX XXX XXX| +|XXX XXX X4X XXX XX5| +|6 | +| | +| 7 | +| | +| 8| ++---------------------------+ +9 +``` +The following positions are being tested: +1. `origin`: buffer origin +2. `midTop`: middle of the top line +3. `midHistory`: middle of the history +4. `midDocEnd`: middle of the last line of text +5. `lastCharPos`: last character in the buffer +6. `docEnd`: one past the last line of text +7. `midEmptySpace`: middle of the empty space in the buffer +8. `bufferEnd`: end of the buffer +9. `endExclusive`: exclusive end of the buffer + +This is intended to provide adequate testing coverage for GH#6986. + +# Variable Heuristics +Each position above already has a predefined variable name. However, a few heuristics are used to define new variables based on the standard variables above. +- `Left`: the left-most position on the same line as `` +- `PC`, `MC`: + - ``: start at the position of `` + - `P` (or `M`): move forwards (aka "plus") by a certain amount (`M` is used to move backwards [aka "minus"]) + - ``: how much to move forwards by + - `C`: move by character. For simplicity, assumes that each character is one-cell wide. +- `PL`, `ML`: + - same as above, except move by line. For simplicity, assumes that you won't hit a buffer boundary. + +# Helpful terms and concepts +- *degenerate*: the text range encompasses no text. Also, means the start and end endpoints are the same. +- *TextUnit*: a heuristic for how much to move by. Possible values include `TextUnit_Character`, `TextUnit_Word`, and `TextUnit_Line`. See https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-uiautomationtextunits for more details. diff --git a/tools/TestTableWriter/UiaTests.csv b/tools/TestTableWriter/UiaTests.csv new file mode 100644 index 000000000..195f6a8ef --- /dev/null +++ b/tools/TestTableWriter/UiaTests.csv @@ -0,0 +1,256 @@ +Degenerate,Position,TextUnit,MoveAmount,Start,End,Result_MoveAmount,Result_Start,Result_End,Skip +TRUE,1,TextUnit_Character,-5,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Character,-1,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Character,0,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Character,1,origin,origin,1,originP1C,originP1C,FALSE +TRUE,1,TextUnit_Character,5,origin,origin,5,originP5C,originP5C,FALSE +FALSE,1,TextUnit_Character,-5,origin,originP1C,0,origin,originP1C,FALSE +FALSE,1,TextUnit_Character,-1,origin,originP1C,0,origin,originP1C,FALSE +FALSE,1,TextUnit_Character,0,origin,originP1C,0,origin,originP1C,FALSE +FALSE,1,TextUnit_Character,1,origin,originP1C,1,originP1C,originP2C,FALSE +FALSE,1,TextUnit_Character,5,origin,originP1C,5,originP5C,originP6C,FALSE +TRUE,2,TextUnit_Character,-5,midTop,midTop,-5,midTopM5C,midTopM5C,FALSE +TRUE,2,TextUnit_Character,-1,midTop,midTop,-1,midTopM1C,midTopM1C,FALSE +TRUE,2,TextUnit_Character,0,midTop,midTop,0,midTop,midTop,FALSE +TRUE,2,TextUnit_Character,1,midTop,midTop,1,midTopP1C,midTopP1C,FALSE +TRUE,2,TextUnit_Character,5,midTop,midTop,5,midTopP5C,midTopP5C,FALSE +FALSE,2,TextUnit_Character,-5,midTop,midTopP1C,-5,midTopM5C,midTopM4C,FALSE +FALSE,2,TextUnit_Character,-1,midTop,midTopP1C,-1,midTopM1C,midTop,FALSE +FALSE,2,TextUnit_Character,0,midTop,midTopP1C,0,midTop,midTopP1C,FALSE +FALSE,2,TextUnit_Character,1,midTop,midTopP1C,1,midTopP1C,midTopP2C,FALSE +FALSE,2,TextUnit_Character,5,midTop,midTopP1C,5,midTopP5C,midTopP6C,FALSE +TRUE,3,TextUnit_Character,-5,midHistory,midHistory,-5,midHistoryM5C,midHistoryM5C,FALSE +TRUE,3,TextUnit_Character,-1,midHistory,midHistory,-1,midHistoryM1C,midHistoryM1C,FALSE +TRUE,3,TextUnit_Character,0,midHistory,midHistory,0,midHistory,midHistory,FALSE +TRUE,3,TextUnit_Character,1,midHistory,midHistory,1,midHistoryP1C,midHistoryP1C,FALSE +TRUE,3,TextUnit_Character,5,midHistory,midHistory,5,midHistoryP5C,midHistoryP5C,FALSE +FALSE,3,TextUnit_Character,-5,midHistory,midHistoryP1C,-5,midHistoryM5C,midHistoryM4C,FALSE +FALSE,3,TextUnit_Character,-1,midHistory,midHistoryP1C,-1,midHistoryM1C,midHistory,FALSE +FALSE,3,TextUnit_Character,0,midHistory,midHistoryP1C,0,midHistory,midHistoryP1C,FALSE +FALSE,3,TextUnit_Character,1,midHistory,midHistoryP1C,1,midHistoryP1C,midHistoryP2C,FALSE +FALSE,3,TextUnit_Character,5,midHistory,midHistoryP1C,5,midHistoryP5C,midHistoryP6C,FALSE +TRUE,1,TextUnit_Line,-5,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Line,-1,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Line,0,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Line,1,origin,origin,1,originP1L,originP1L,FALSE +TRUE,1,TextUnit_Line,5,origin,origin,5,originP5L,originP5L,FALSE +FALSE,1,TextUnit_Line,-5,origin,originP1C,0,origin,originP1L,TRUE +FALSE,1,TextUnit_Line,-1,origin,originP1C,0,origin,originP1L,TRUE +FALSE,1,TextUnit_Line,0,origin,originP1C,0,origin,originP1L,TRUE +FALSE,1,TextUnit_Line,1,origin,originP1C,1,originP1L,originP2L,FALSE +FALSE,1,TextUnit_Line,5,origin,originP1C,5,originP5L,originP6L,FALSE +TRUE,2,TextUnit_Line,-5,midTop,midTop,-1,origin,origin,TRUE +TRUE,2,TextUnit_Line,-1,midTop,midTop,-1,origin,origin,TRUE +TRUE,2,TextUnit_Line,0,midTop,midTop,0,midTop,midTop,FALSE +TRUE,2,TextUnit_Line,1,midTop,midTop,1,midTopP1L,midTopP1L,FALSE +TRUE,2,TextUnit_Line,5,midTop,midTop,5,midTopP5L,midTopP5L,FALSE +FALSE,2,TextUnit_Line,-5,midTop,midTopP1C,0,origin,originP1L,TRUE +FALSE,2,TextUnit_Line,-1,midTop,midTopP1C,0,origin,originP1L,TRUE +FALSE,2,TextUnit_Line,0,midTop,midTopP1C,0,origin,originP1L,TRUE +FALSE,2,TextUnit_Line,1,midTop,midTopP1C,1,originP1L,originP2L,FALSE +FALSE,2,TextUnit_Line,5,midTop,midTopP1C,5,originP5L,originP6L,FALSE +TRUE,3,TextUnit_Line,-5,midHistory,midHistory,-5,midHistoryM4L,midHistoryM4L,FALSE +TRUE,3,TextUnit_Line,-1,midHistory,midHistory,-1,midHistoryLeft,midHistoryLeft,FALSE +TRUE,3,TextUnit_Line,0,midHistory,midHistory,0,midHistory,midHistory,FALSE +TRUE,3,TextUnit_Line,1,midHistory,midHistory,1,midHistoryP1L,midHistoryP1L,FALSE +TRUE,3,TextUnit_Line,5,midHistory,midHistory,5,midHistoryP5L,midHistoryP5L,FALSE +FALSE,3,TextUnit_Line,-5,midHistory,midHistoryP1C,-5,midHistoryM5L,midHistoryM4L,TRUE +FALSE,3,TextUnit_Line,-1,midHistory,midHistoryP1C,-1,midHistoryM1L,midHistoryLeft,TRUE +FALSE,3,TextUnit_Line,0,midHistory,midHistoryP1C,0,midHistoryLeft,midHistoryP1L,TRUE +FALSE,3,TextUnit_Line,1,midHistory,midHistoryP1C,1,midHistoryP1L,midHistoryP2L,FALSE +FALSE,3,TextUnit_Line,5,midHistory,midHistoryP1C,5,midHistoryP5L,midHistoryP6L,FALSE +TRUE,1,TextUnit_Document,-5,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Document,-1,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Document,0,origin,origin,0,origin,origin,FALSE +TRUE,1,TextUnit_Document,1,origin,origin,1,docEnd,docEnd,TRUE +TRUE,1,TextUnit_Document,5,origin,origin,1,docEnd,docEnd,TRUE +FALSE,1,TextUnit_Document,-5,origin,originP1C,0,origin,docEnd,TRUE +FALSE,1,TextUnit_Document,-1,origin,originP1C,0,origin,docEnd,TRUE +FALSE,1,TextUnit_Document,0,origin,originP1C,0,origin,docEnd,TRUE +FALSE,1,TextUnit_Document,1,origin,originP1C,0,origin,docEnd,TRUE +FALSE,1,TextUnit_Document,5,origin,originP1C,0,origin,docEnd,TRUE +TRUE,2,TextUnit_Document,-5,midTop,midTop,-1,origin,origin,FALSE +TRUE,2,TextUnit_Document,-1,midTop,midTop,-1,origin,origin,FALSE +TRUE,2,TextUnit_Document,0,midTop,midTop,0,midTop,midTop,FALSE +TRUE,2,TextUnit_Document,1,midTop,midTop,1,bufferEnd,bufferEnd,TRUE +TRUE,2,TextUnit_Document,5,midTop,midTop,1,bufferEnd,bufferEnd,TRUE +FALSE,2,TextUnit_Document,-5,midTop,midTopP1C,0,origin,bufferEnd,TRUE +FALSE,2,TextUnit_Document,-1,midTop,midTopP1C,0,origin,bufferEnd,TRUE +FALSE,2,TextUnit_Document,0,midTop,midTopP1C,0,origin,bufferEnd,TRUE +FALSE,2,TextUnit_Document,1,midTop,midTopP1C,0,origin,bufferEnd,TRUE +FALSE,2,TextUnit_Document,5,midTop,midTopP1C,0,origin,bufferEnd,TRUE +TRUE,3,TextUnit_Document,-5,midHistory,midHistory,-1,origin,origin,FALSE +TRUE,3,TextUnit_Document,-1,midHistory,midHistory,-1,origin,origin,FALSE +TRUE,3,TextUnit_Document,0,midHistory,midHistory,0,midHistory,midHistory,FALSE +TRUE,3,TextUnit_Document,1,midHistory,midHistory,1,endExclusive,endExclusive,TRUE +TRUE,3,TextUnit_Document,5,midHistory,midHistory,1,endExclusive,endExclusive,TRUE +FALSE,3,TextUnit_Document,-5,midHistory,midHistoryP1C,0,origin,endExclusive,TRUE +FALSE,3,TextUnit_Document,-1,midHistory,midHistoryP1C,0,origin,endExclusive,TRUE +FALSE,3,TextUnit_Document,0,midHistory,midHistoryP1C,0,origin,endExclusive,TRUE +FALSE,3,TextUnit_Document,1,midHistory,midHistoryP1C,0,origin,endExclusive,TRUE +FALSE,3,TextUnit_Document,5,midHistory,midHistoryP1C,0,origin,endExclusive,TRUE +TRUE,8,TextUnit_Character,-5,bufferEnd,bufferEnd,-5,bufferEndM5C,bufferEndM5C,FALSE +TRUE,8,TextUnit_Character,-1,bufferEnd,bufferEnd,-1,bufferEndM1C,bufferEndM1C,FALSE +TRUE,8,TextUnit_Character,0,bufferEnd,bufferEnd,0,bufferEnd,bufferEnd,FALSE +TRUE,8,TextUnit_Character,1,bufferEnd,bufferEnd,1,endExclusive,endExclusive,TRUE +TRUE,8,TextUnit_Character,5,bufferEnd,bufferEnd,1,endExclusive,endExclusive,TRUE +FALSE,8,TextUnit_Character,-5,bufferEnd,endExclusive,-5,bufferEndM5C,bufferEndM4C,FALSE +FALSE,8,TextUnit_Character,-1,bufferEnd,endExclusive,-1,bufferEndM1C,bufferEnd,FALSE +FALSE,8,TextUnit_Character,0,bufferEnd,endExclusive,0,bufferEnd,endExclusive,FALSE +FALSE,8,TextUnit_Character,1,bufferEnd,endExclusive,0,bufferEnd,endExclusive,FALSE +FALSE,8,TextUnit_Character,5,bufferEnd,endExclusive,0,bufferEnd,endExclusive,FALSE +TRUE,8,TextUnit_Line,-5,bufferEnd,bufferEnd,-5,bufferEndM4L,bufferEndM4L,FALSE +TRUE,8,TextUnit_Line,-1,bufferEnd,bufferEnd,-1,bufferEndLeft,bufferEndLeft,FALSE +TRUE,8,TextUnit_Line,0,bufferEnd,bufferEnd,0,bufferEnd,bufferEnd,FALSE +TRUE,8,TextUnit_Line,1,bufferEnd,bufferEnd,1,endExclusive,endExclusive,TRUE +TRUE,8,TextUnit_Line,5,bufferEnd,bufferEnd,1,endExclusive,endExclusive,TRUE +FALSE,8,TextUnit_Line,-5,bufferEnd,endExclusive,-5,bufferEndM5L,bufferEndM4L,TRUE +FALSE,8,TextUnit_Line,-1,bufferEnd,endExclusive,-1,bufferEndM1L,bufferEndLeft,TRUE +FALSE,8,TextUnit_Line,0,bufferEnd,endExclusive,0,bufferEndLeft,endExclusive,TRUE +FALSE,8,TextUnit_Line,1,bufferEnd,endExclusive,0,bufferEndLeft,endExclusive,TRUE +FALSE,8,TextUnit_Line,5,bufferEnd,endExclusive,0,bufferEndLeft,endExclusive,TRUE +TRUE,8,TextUnit_Document,-5,bufferEnd,bufferEnd,-1,origin,origin,FALSE +TRUE,8,TextUnit_Document,-1,bufferEnd,bufferEnd,-1,origin,origin,FALSE +TRUE,8,TextUnit_Document,0,bufferEnd,bufferEnd,0,bufferEnd,bufferEnd,FALSE +TRUE,8,TextUnit_Document,1,bufferEnd,bufferEnd,1,endExclusive,endExclusive,TRUE +TRUE,8,TextUnit_Document,5,bufferEnd,bufferEnd,1,endExclusive,endExclusive,TRUE +FALSE,8,TextUnit_Document,-5,bufferEnd,endExclusive,0,origin,endExclusive,TRUE +FALSE,8,TextUnit_Document,-1,bufferEnd,endExclusive,0,origin,endExclusive,TRUE +FALSE,8,TextUnit_Document,0,bufferEnd,endExclusive,0,origin,endExclusive,TRUE +FALSE,8,TextUnit_Document,1,bufferEnd,endExclusive,0,origin,endExclusive,TRUE +FALSE,8,TextUnit_Document,5,bufferEnd,endExclusive,0,origin,endExclusive,TRUE +TRUE,9,TextUnit_Character,-5,endExclusive,endExclusive,-5,bufferEndM4C,bufferEndM4C,FALSE +TRUE,9,TextUnit_Character,-1,endExclusive,endExclusive,-1,bufferEnd,bufferEnd,FALSE +TRUE,9,TextUnit_Character,0,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Character,1,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Character,5,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Line,-5,endExclusive,endExclusive,-5,bufferEndM4L,bufferEndM4L,FALSE +TRUE,9,TextUnit_Line,-1,endExclusive,endExclusive,-1,bufferEndLeft,bufferEndLeft,FALSE +TRUE,9,TextUnit_Line,0,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Line,1,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Line,5,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Document,-5,endExclusive,endExclusive,-1,origin,origin,FALSE +TRUE,9,TextUnit_Document,-1,endExclusive,endExclusive,-1,origin,origin,FALSE +TRUE,9,TextUnit_Document,0,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Document,1,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,9,TextUnit_Document,5,endExclusive,endExclusive,0,endExclusive,endExclusive,FALSE +TRUE,4,TextUnit_Character,-5,midDocEnd,midDocEnd,-5,midDocEndM5C,midDocEndM5C,TRUE +TRUE,4,TextUnit_Character,-1,midDocEnd,midDocEnd,-1,midDocEndM1C,midDocEndM1C,TRUE +TRUE,4,TextUnit_Character,0,midDocEnd,midDocEnd,0,midDocEnd,midDocEnd,TRUE +TRUE,4,TextUnit_Character,1,midDocEnd,midDocEnd,1,midDocEndP1C,midDocEndP1C,TRUE +TRUE,4,TextUnit_Character,5,midDocEnd,midDocEnd,5,midDocEndP5C,midDocEndP5C,TRUE +FALSE,4,TextUnit_Character,-5,midDocEnd,midDocEndP1C,-5,midDocEndM5C,midDocEndM4C,TRUE +FALSE,4,TextUnit_Character,-1,midDocEnd,midDocEndP1C,-1,midDocEndM1C,midDocEnd,TRUE +FALSE,4,TextUnit_Character,0,midDocEnd,midDocEndP1C,0,midDocEnd,midDocEndP1C,TRUE +FALSE,4,TextUnit_Character,1,midDocEnd,midDocEndP1C,1,midDocEndP1C,midDocEndP2C,TRUE +FALSE,4,TextUnit_Character,5,midDocEnd,midDocEndP1C,5,midDocEndP5C,midDocEndP6C,TRUE +TRUE,4,TextUnit_Line,-5,midDocEnd,midDocEnd,-5,midDocEndM4L,midDocEndM4L,TRUE +TRUE,4,TextUnit_Line,-1,midDocEnd,midDocEnd,-1,midDocEndLeft,midDocEndLeft,TRUE +TRUE,4,TextUnit_Line,0,midDocEnd,midDocEnd,0,midDocEnd,midDocEnd,TRUE +TRUE,4,TextUnit_Line,1,midDocEnd,midDocEnd,1,docEnd,docEnd,TRUE +TRUE,4,TextUnit_Line,5,midDocEnd,midDocEnd,1,docEnd,docEnd,TRUE +FALSE,4,TextUnit_Line,-5,midDocEnd,midDocEndP1C,-5,midDocEndM5L,midDocEndM4L,TRUE +FALSE,4,TextUnit_Line,-1,midDocEnd,midDocEndP1C,-1,midDocEndM1L,midDocEndLeft,TRUE +FALSE,4,TextUnit_Line,0,midDocEnd,midDocEndP1C,0,midDocEndLeft,docEnd,TRUE +FALSE,4,TextUnit_Line,1,midDocEnd,midDocEndP1C,0,midDocEndLeft,docEnd,TRUE +FALSE,4,TextUnit_Line,5,midDocEnd,midDocEndP1C,0,midDocEndLeft,docEnd,TRUE +TRUE,4,TextUnit_Document,-5,midDocEnd,midDocEnd,-1,origin,origin,TRUE +TRUE,4,TextUnit_Document,-1,midDocEnd,midDocEnd,-1,origin,origin,TRUE +TRUE,4,TextUnit_Document,0,midDocEnd,midDocEnd,0,midDocEnd,midDocEnd,TRUE +TRUE,4,TextUnit_Document,1,midDocEnd,midDocEnd,1,docEnd,docEnd,TRUE +TRUE,4,TextUnit_Document,5,midDocEnd,midDocEnd,1,docEnd,docEnd,TRUE +FALSE,4,TextUnit_Document,-5,midDocEnd,midDocEndP1C,0,origin,docEnd,TRUE +FALSE,4,TextUnit_Document,-1,midDocEnd,midDocEndP1C,0,origin,docEnd,TRUE +FALSE,4,TextUnit_Document,0,midDocEnd,midDocEndP1C,0,origin,docEnd,TRUE +FALSE,4,TextUnit_Document,1,midDocEnd,midDocEndP1C,0,origin,docEnd,TRUE +FALSE,4,TextUnit_Document,5,midDocEnd,midDocEndP1C,0,origin,docEnd,TRUE +TRUE,5,TextUnit_Character,-5,lastCharPos,lastCharPos,-5,lastCharPosM5C,lastCharPosM5C,TRUE +TRUE,5,TextUnit_Character,-1,lastCharPos,lastCharPos,-1,lastCharPosM1C,lastCharPosM1C,TRUE +TRUE,5,TextUnit_Character,0,lastCharPos,lastCharPos,0,lastCharPos,lastCharPos,TRUE +TRUE,5,TextUnit_Character,1,lastCharPos,lastCharPos,1,docEnd,docEnd,TRUE +TRUE,5,TextUnit_Character,5,lastCharPos,lastCharPos,1,docEnd,docEnd,TRUE +FALSE,5,TextUnit_Character,-5,lastCharPos,lastCharPosP1C,-5,lastCharPosM5C,lastCharPosM4C,TRUE +FALSE,5,TextUnit_Character,-1,lastCharPos,lastCharPosP1C,-1,lastCharPosM1C,lastCharPos,TRUE +FALSE,5,TextUnit_Character,0,lastCharPos,lastCharPosP1C,0,lastCharPos,docEnd,TRUE +FALSE,5,TextUnit_Character,1,lastCharPos,lastCharPosP1C,0,lastCharPos,docEnd,TRUE +FALSE,5,TextUnit_Character,5,lastCharPos,lastCharPosP1C,0,lastCharPos,docEnd,TRUE +TRUE,5,TextUnit_Line,-5,lastCharPos,lastCharPos,-5,lastCharPosM4L,lastCharPosM4L,TRUE +TRUE,5,TextUnit_Line,-1,lastCharPos,lastCharPos,-1,lastCharPosLeft,lastCharPosLeft,TRUE +TRUE,5,TextUnit_Line,0,lastCharPos,lastCharPos,0,lastCharPos,lastCharPos,TRUE +TRUE,5,TextUnit_Line,1,lastCharPos,lastCharPos,1,docEnd,docEnd,TRUE +TRUE,5,TextUnit_Line,5,lastCharPos,lastCharPos,1,docEnd,docEnd,TRUE +FALSE,5,TextUnit_Line,-5,lastCharPos,lastCharPosP1C,-5,lastCharPosM5L,lastCharPosM4L,TRUE +FALSE,5,TextUnit_Line,-1,lastCharPos,lastCharPosP1C,-1,lastCharPosM1L,lastCharPosLeft,TRUE +FALSE,5,TextUnit_Line,0,lastCharPos,lastCharPosP1C,0,lastCharPosLeft,docEnd,TRUE +FALSE,5,TextUnit_Line,1,lastCharPos,lastCharPosP1C,0,lastCharPosLeft,docEnd,TRUE +FALSE,5,TextUnit_Line,5,lastCharPos,lastCharPosP1C,0,lastCharPosLeft,docEnd,TRUE +TRUE,5,TextUnit_Document,-5,lastCharPos,lastCharPos,-1,origin,origin,TRUE +TRUE,5,TextUnit_Document,-1,lastCharPos,lastCharPos,-1,origin,origin,TRUE +TRUE,5,TextUnit_Document,0,lastCharPos,lastCharPos,0,lastCharPos,lastCharPos,TRUE +TRUE,5,TextUnit_Document,1,lastCharPos,lastCharPos,1,docEnd,docEnd,TRUE +TRUE,5,TextUnit_Document,5,lastCharPos,lastCharPos,1,docEnd,docEnd,TRUE +FALSE,5,TextUnit_Document,-5,lastCharPos,lastCharPosP1C,0,origin,docEnd,TRUE +FALSE,5,TextUnit_Document,-1,lastCharPos,lastCharPosP1C,0,origin,docEnd,TRUE +FALSE,5,TextUnit_Document,0,lastCharPos,lastCharPosP1C,0,origin,docEnd,TRUE +FALSE,5,TextUnit_Document,1,lastCharPos,lastCharPosP1C,0,origin,docEnd,TRUE +FALSE,5,TextUnit_Document,5,lastCharPos,lastCharPosP1C,0,origin,docEnd,TRUE +TRUE,6,TextUnit_Character,-5,docEnd,docEnd,-5,docEndM5C,docEndM5C,TRUE +TRUE,6,TextUnit_Character,-1,docEnd,docEnd,-1,docEndM1C,docEndM1C,TRUE +TRUE,6,TextUnit_Character,0,docEnd,docEnd,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Character,1,docEnd,docEnd,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Character,5,docEnd,docEnd,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Character,-5,docEnd,docEndP1C,-5,docEndM5C,docEndM5C,TRUE +FALSE,6,TextUnit_Character,-1,docEnd,docEndP1C,-1,docEndM1C,docEndM1C,TRUE +FALSE,6,TextUnit_Character,0,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Character,1,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Character,5,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Line,-5,docEnd,docEnd,-5,docEndM4L,docEndM4L,TRUE +TRUE,6,TextUnit_Line,-1,docEnd,docEnd,-1,docEndLeft,docEndLeft,TRUE +TRUE,6,TextUnit_Line,0,docEnd,docEnd,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Line,1,docEnd,docEnd,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Line,5,docEnd,docEnd,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Line,-5,docEnd,docEndP1C,-5,docEndM4L,docEndM4L,TRUE +FALSE,6,TextUnit_Line,-1,docEnd,docEndP1C,-1,docEndLeft,docEndLeft,TRUE +FALSE,6,TextUnit_Line,0,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Line,1,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Line,5,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Document,-5,docEnd,docEnd,-1,origin,origin,TRUE +TRUE,6,TextUnit_Document,-1,docEnd,docEnd,-1,origin,origin,TRUE +TRUE,6,TextUnit_Document,0,docEnd,docEnd,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Document,1,docEnd,docEnd,0,docEnd,docEnd,TRUE +TRUE,6,TextUnit_Document,5,docEnd,docEnd,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Document,-5,docEnd,docEndP1C,-1,origin,origin,TRUE +FALSE,6,TextUnit_Document,-1,docEnd,docEndP1C,-1,origin,origin,TRUE +FALSE,6,TextUnit_Document,0,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Document,1,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +FALSE,6,TextUnit_Document,5,docEnd,docEndP1C,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Character,-5,midEmptySpace,midEmptySpace,-5,docEndM5C,docEndM5C,TRUE +TRUE,7,TextUnit_Character,-1,midEmptySpace,midEmptySpace,-1,docEndM1C,docEndM1C,TRUE +TRUE,7,TextUnit_Character,0,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Character,1,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Character,5,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Character,-5,midEmptySpace,midEmptySpaceP1C,-5,docEndM5C,docEndM5C,TRUE +FALSE,7,TextUnit_Character,-1,midEmptySpace,midEmptySpaceP1C,-1,docEndM1C,docEndM1C,TRUE +FALSE,7,TextUnit_Character,0,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Character,1,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Character,5,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Line,-5,midEmptySpace,midEmptySpace,-5,docEndM4L,docEndM4L,TRUE +TRUE,7,TextUnit_Line,-1,midEmptySpace,midEmptySpace,-1,docEndLeft,docEndLeft,TRUE +TRUE,7,TextUnit_Line,0,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Line,1,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Line,5,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Line,-5,midEmptySpace,midEmptySpaceP1C,-5,docEndM4L,docEndM4L,TRUE +FALSE,7,TextUnit_Line,-1,midEmptySpace,midEmptySpaceP1C,-1,docEndLeft,docEndLeft,TRUE +FALSE,7,TextUnit_Line,0,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Line,1,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Line,5,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Document,-5,midEmptySpace,midEmptySpace,-1,origin,origin,TRUE +TRUE,7,TextUnit_Document,-1,midEmptySpace,midEmptySpace,-1,origin,origin,TRUE +TRUE,7,TextUnit_Document,0,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Document,1,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +TRUE,7,TextUnit_Document,5,midEmptySpace,midEmptySpace,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Document,-5,midEmptySpace,midEmptySpaceP1C,-1,origin,origin,TRUE +FALSE,7,TextUnit_Document,-1,midEmptySpace,midEmptySpaceP1C,-1,origin,origin,TRUE +FALSE,7,TextUnit_Document,0,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Document,1,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE +FALSE,7,TextUnit_Document,5,midEmptySpace,midEmptySpaceP1C,0,docEnd,docEnd,TRUE diff --git a/tools/tests.xml b/tools/tests.xml index fd6444662..c6085b0b4 100644 --- a/tools/tests.xml +++ b/tools/tests.xml @@ -4,8 +4,8 @@ - - + +