From f4d8a74082be50783c5d5c92c5bd631f0dfe938c Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:36:13 -0500 Subject: [PATCH 01/53] Localization Updates - main - associated with #16886 (#17035) --- .../Resources/de-DE/Resources.resw | 20 +++---- .../Resources/es-ES/Resources.resw | 20 +++---- .../Resources/fr-FR/Resources.resw | 26 ++++---- .../Resources/it-IT/Resources.resw | 16 ++--- .../Resources/ja-JP/Resources.resw | 24 ++++---- .../Resources/ko-KR/Resources.resw | 28 ++++----- .../Resources/pt-BR/Resources.resw | 48 +++++++-------- .../Resources/qps-ploc/Resources.resw | 60 +++++++++---------- .../Resources/qps-ploca/Resources.resw | 60 +++++++++---------- .../Resources/qps-plocm/Resources.resw | 60 +++++++++---------- .../Resources/ru-RU/Resources.resw | 14 ++--- .../Resources/zh-CN/Resources.resw | 14 ++--- .../Resources/zh-TW/Resources.resw | 18 +++--- .../Resources/de-DE/Resources.resw | 22 +++---- .../Resources/es-ES/Resources.resw | 20 +++---- .../Resources/fr-FR/Resources.resw | 22 +++---- .../Resources/it-IT/Resources.resw | 22 +++---- .../Resources/ja-JP/Resources.resw | 20 +++---- .../Resources/ko-KR/Resources.resw | 18 +++--- .../Resources/pt-BR/Resources.resw | 18 +++--- .../Resources/qps-ploc/Resources.resw | 22 +++---- .../Resources/qps-ploca/Resources.resw | 22 +++---- .../Resources/qps-plocm/Resources.resw | 22 +++---- .../Resources/ru-RU/Resources.resw | 22 +++---- .../Resources/zh-CN/Resources.resw | 22 +++---- .../Resources/zh-TW/Resources.resw | 22 +++---- 26 files changed, 341 insertions(+), 341 deletions(-) diff --git a/src/cascadia/TerminalApp/Resources/de-DE/Resources.resw b/src/cascadia/TerminalApp/Resources/de-DE/Resources.resw index 948fec8ff6..eb9e1a40c0 100644 --- a/src/cascadia/TerminalApp/Resources/de-DE/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/de-DE/Resources.resw @@ -190,16 +190,16 @@ Mehrere Bereiche - Schließen... + Schließen - Registerkarten auf der rechten Seite schließen + Tabs nach rechts schließen Andere Registerkarten schließen - Registerkarte schließen + Tab schließen Bereich schließen @@ -208,16 +208,16 @@ Registerkarte teilen - Geteilter Bereich + Bereich teilen Websuche - Farbe... + Registerkartenfarbe ändern - Benutzerdefiniert... + Benutzerdefiniert Zurücksetzen @@ -226,7 +226,7 @@ Registerkarte umbenennen - Registerkarte duplizieren + Registerkarte kopieren Profil mit einem ungültigen "backgroundImage" gefunden. Dieses Profil hat standardmäßig kein Hintergrundbild. Stellen Sie sicher, dass beim Festlegen eines "backgroundImage" der Wert ein gültiger Dateipfad zu einem Bild ist. @@ -487,7 +487,7 @@ A hyperlink name for the Terminal's privacy policy - Drittanbieter-Hinweise + Hinweise von Drittanbietern A hyperlink name for the Terminal's third-party notices @@ -761,7 +761,7 @@ Suchen - Nur Text + Unformatierter Text Das Beendigungsverhalten kann in den erweiterten Profileinstellungen konfiguriert werden. @@ -830,7 +830,7 @@ Diese Registerkarte schließen - Leer... + Leer Bereich schließen diff --git a/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw b/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw index 0679cd0ddd..f0ce1d07a4 100644 --- a/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw @@ -187,7 +187,7 @@ Varios paneles - Cerrar... + Cerrar Cerrar las pestañas de la derecha @@ -202,19 +202,19 @@ Cerrar panel - Dividir tabla + Dividir pestaña Panel dividido - Búsqueda en la Web + Búsqueda en la web - Color... + Cambiar color de pestaña - Configuración personalizada... + Personalizar Restablecer @@ -464,7 +464,7 @@ This is the heading for a version number label - Tareas iniciales + Introducción A hyperlink name for a guide on how to get started using Terminal @@ -480,7 +480,7 @@ A hyperlink name for the Terminal's release notes - Directiva de privacidad + Política de privacidad A hyperlink name for the Terminal's privacy policy @@ -723,7 +723,7 @@ Maximizar - Restaurar a tamaño normal + Restaurar Paleta de comandos @@ -827,7 +827,7 @@ Cerrar esta pestaña - Vacío... + Vacío Cerrar panel @@ -840,7 +840,7 @@ Text used to identify the reset button - Mover la Pestaña a una Nueva ventana + Mover pestaña a nueva ventana Mueve la pestaña a una nueva ventana diff --git a/src/cascadia/TerminalApp/Resources/fr-FR/Resources.resw b/src/cascadia/TerminalApp/Resources/fr-FR/Resources.resw index 887f79be42..68f9c84a82 100644 --- a/src/cascadia/TerminalApp/Resources/fr-FR/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/fr-FR/Resources.resw @@ -187,13 +187,13 @@ Volets multiples - Fermez... + Fermer - Fermer les Onglets à Droite + Fermer les onglets à droite - Fermez les Autres onglets + Fermer les autres onglets Fermer l’onglet @@ -205,16 +205,16 @@ Fractionner l’onglet - Fractionner le volet... + Fractionner le volet Recherche web - Couleur... + Modifier la couleur de l’onglet - Personnalisée... + Personnalisé Réinitialiser @@ -464,7 +464,7 @@ This is the heading for a version number label - Prise en main + Bien démarrer A hyperlink name for a guide on how to get started using Terminal @@ -480,11 +480,11 @@ A hyperlink name for the Terminal's release notes - Politique de confidentialité + Charte de confidentialité A hyperlink name for the Terminal's privacy policy - Avis de tiers + Mentions tierces A hyperlink name for the Terminal's third-party notices @@ -723,7 +723,7 @@ Agrandir - Niveau inférieur + Restaurer Palette de commandes @@ -767,7 +767,7 @@ Ne plus afficher - Cette fenêtre de terminal s’exécute en tant qu’Administrateur + Cette fenêtre de terminal s’exécute en tant qu’administrateur {0} suggestions trouvées @@ -827,7 +827,7 @@ Fermer cet onglet - Vide... + Vide Fermer le volet @@ -846,7 +846,7 @@ Déplacer l'onglet vers une nouvelle fenêtre - Exécuter en tant qu'administrateur + Exécuter en temps qu'administrateur (restreint) This text is displayed on context menu for profile entries in add new tab button. diff --git a/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw b/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw index 69122195db..0f1b1a0b14 100644 --- a/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw @@ -187,7 +187,7 @@ Più riquadri - Chiudi... + Chiudi Chiudi schede a destra @@ -199,7 +199,7 @@ Chiudi scheda - Chiudi riquadro + Chiudi il riquadro Dividi scheda @@ -211,10 +211,10 @@ Ricerca nel Web - Colore... + Cambia colore scheda - Personalizzato... + Personalizzato Reimposta @@ -464,7 +464,7 @@ This is the heading for a version number label - Attività iniziali + Introduzione A hyperlink name for a guide on how to get started using Terminal @@ -484,7 +484,7 @@ A hyperlink name for the Terminal's privacy policy - Informative di terze parti + Comunicazioni di terze parti A hyperlink name for the Terminal's third-party notices @@ -723,7 +723,7 @@ Ingrandisci - Ripristina visualizzazione normale + Ripristina in basso Riquadro comandi @@ -830,7 +830,7 @@ Vuoto - Chiudi riquadro + Chiudi il riquadro Chiude il riquadro attivo se sono presenti più riquadri diff --git a/src/cascadia/TerminalApp/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalApp/Resources/ja-JP/Resources.resw index 3451318e93..e9141817fc 100644 --- a/src/cascadia/TerminalApp/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/ja-JP/Resources.resw @@ -188,7 +188,7 @@ 複数ウィンドウ - 閉じる... + 閉じる タブを右側に閉じる @@ -203,28 +203,28 @@ ウィンドウを閉じる - 分割タブ + [分割] タブ - 分割ウィンドウ + ウィンドウを分割する Web 検索 - 色... + タブの色の変更 - カスタム... + カスタム リセット - タブ名を変更 + [名前の変更] タブ - タブの複製 + タブを複製する 無効な "backgroundImage" を持つプロファイルが見つかりました。既定では、そのプロファイルに背景画像はありません。"backgroundImage" を設定するときに、値が画像への有効なファイル パスとなっていることをご確認ください。 @@ -485,7 +485,7 @@ A hyperlink name for the Terminal's privacy policy - サードパーティに関する通知 + サード パーティ通知 A hyperlink name for the Terminal's third-party notices @@ -724,7 +724,7 @@ 最大化 - 元に戻す + 元に戻す (縮小) コマンド パレット @@ -759,7 +759,7 @@ 検索する - プレーンテキスト + テキスト 終了動作は、プロファイルの詳細設定で構成できます。 @@ -828,7 +828,7 @@ このタブを閉じます - 空っぽ... + なし ウィンドウを閉じる @@ -847,7 +847,7 @@ タブを新しいウィンドウに移動 - 管理者として実行する + 管理者として実行 This text is displayed on context menu for profile entries in add new tab button. diff --git a/src/cascadia/TerminalApp/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalApp/Resources/ko-KR/Resources.resw index 2b7ebd7514..2f9e77e920 100644 --- a/src/cascadia/TerminalApp/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/ko-KR/Resources.resw @@ -187,10 +187,10 @@ 여러 창 - 닫기... + 닫기 - 오른쪽으로 탭 닫기 + 오른쪽에 있는 탭 닫기 다른 탭 닫기 @@ -211,10 +211,10 @@ 웹 검색 - 색... + 탭 색 변경 - 사용자 정의... + 사용자 지정 다시 설정 @@ -454,7 +454,7 @@ 정보 - 피드백 보내기 + 의견 보내기 확인 @@ -468,7 +468,7 @@ A hyperlink name for a guide on how to get started using Terminal - 소스 코드 + 원본 코드 A hyperlink name for the Terminal's documentation @@ -480,11 +480,11 @@ A hyperlink name for the Terminal's release notes - 개인정보 취급방침 + 개인정보 보호정책 A hyperlink name for the Terminal's privacy policy - 타사 통지 사항 + 타사 통지 A hyperlink name for the Terminal's third-party notices @@ -571,7 +571,7 @@ 명령줄 구문 분석 오류: - 명령 도구 모음 + 명령 팔레트 탭 전환기 @@ -723,10 +723,10 @@ 최대화 - 아래로 복원 + 이전 크기로 복원 - 명령 도구 모음 + 명령 팔레트 포커스 터미널 @@ -827,7 +827,7 @@ 이 탭 닫기 - 비어 있음... + 비어 있음 창 닫기 @@ -840,13 +840,13 @@ Text used to identify the reset button - 새 창으로 탭 이동 + 탭을 새 창으로 이동 탭을 새 창으로 이동 - 관리자로 실행 + 관리자 권한으로 실행 This text is displayed on context menu for profile entries in add new tab button. diff --git a/src/cascadia/TerminalApp/Resources/pt-BR/Resources.resw b/src/cascadia/TerminalApp/Resources/pt-BR/Resources.resw index d94a294641..b6ca19082f 100644 --- a/src/cascadia/TerminalApp/Resources/pt-BR/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/pt-BR/Resources.resw @@ -187,34 +187,34 @@ Vários painéis - Fechar... + Fechar - Fechar Guias à Direita + Fechar guias à direita - Fechar Outras Guias + Fechar outras guias Fechar guia - Fechar Painel + Fechar o painel - Dividir Guia + Guia Dividir - Painel dividido + Dividir painel - Pesquisa na web + Pesquisa na Web - Cor... + Alterar cor da guia - Personalizados... + Personalizado Restaurar @@ -223,7 +223,7 @@ Renomear guia - Duplicar Guia + Duplicar guia Foi encontrado um perfil com um "backgroundImage" inválido. O perfil deve ser o padrão para que não haja nenhuma imagem de tela de fundo. Certifique-se de que, ao definir um "backgroundImage", o valor é um caminho de arquivo válido para uma imagem. @@ -454,7 +454,7 @@ Sobre - Enviar Comentários + Enviar comentários OK @@ -464,7 +464,7 @@ This is the heading for a version number label - Ponto de Partida + Introdução A hyperlink name for a guide on how to get started using Terminal @@ -484,7 +484,7 @@ A hyperlink name for the Terminal's privacy policy - Avisos de terceiros + Avisos de Terceiros A hyperlink name for the Terminal's third-party notices @@ -571,10 +571,10 @@ Falha ao analisar a linha de comando: - Paleta de Comandos + Paleta de comandos - Seletor de guias + Alternador de guias Digite o nome da guia... @@ -726,7 +726,7 @@ Restaurar Tamanho Original - Paleta de Comandos + Paleta de comandos Terminal de foco @@ -746,7 +746,7 @@ Dividir a janela e iniciar em determinado diretório - Exportar Texto + Exportar texto Falha ao exportar o conteúdo do terminal @@ -758,7 +758,7 @@ Localizar - Texto Simples + Texto sem formatação O comportamento de término pode ser configurado nas configurações avançadas do perfil. @@ -767,7 +767,7 @@ Não mostra de novo - Esta janela do Terminal está funcionando como Administrador + Esta janela do Terminal está sendo executada como administrador Sugestões encontradas: {0} @@ -827,10 +827,10 @@ Fechar esta guia - Vazio... + Vazio - Fechar Painel + Fechar o painel Feche o painel ativo se vários painéis estiverem presentes @@ -840,13 +840,13 @@ Text used to identify the reset button - Mover Guia para Nova Janela + Mover guia para nova janela Move a guia para uma nova janela - Executar como Administrador + Executar como administrador This text is displayed on context menu for profile entries in add new tab button. @@ -885,7 +885,7 @@ Se definido, o comando será acrescentado ao comando padrão do perfil em vez de substituí-lo. - Reiniciar Conexão + Reiniciar conexão Reiniciar a conexão do painel ativo diff --git a/src/cascadia/TerminalApp/Resources/qps-ploc/Resources.resw b/src/cascadia/TerminalApp/Resources/qps-ploc/Resources.resw index 0e553d2fc3..67031f290f 100644 --- a/src/cascadia/TerminalApp/Resources/qps-ploc/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/qps-ploc/Resources.resw @@ -191,43 +191,43 @@ Мµļтíрłĕ φдпėŝ !!! ! - Ćļôŝέ... !! + Ćļôŝέ ! - Ċĺοşέ Ţаъş ťό ŧђé Яΐğђт !!! !!! + Ċĺοşέ ţаъş ťό ŧђé яΐğђт !!! !!! - Ćĺόѕ℮ Όтђèř Ŧâьś !!! ! + Ćĺόѕ℮ őтђèř ŧâьś !!! ! - Сĺôšę Ťăв !!! + Сĺôšę ťăв !!! - Ćŀöśё Раņé !!! + Ćŀöśё ρаņé !!! - Šрľīτ Τàв !!! + Šрľīτ τàв !!! - Šрŀіт Ρªňë !!! + Šрŀіт φªňë !!! - Ẅёв Şĕаŕčĥ !!! + Ẅёв şĕаŕčĥ !!! - Ċõŀόř... !! + Ċħāņğě τāв ςōĺöя !!! ! - Ċµѕťøм... !!! + Ċµѕťøм ! Яěšěŧ ! - Γεñамē Ťãв !!! + Γεñамē ťãв !!! - Ďϋφľіčάтέ Τàв !!! + Ďϋφľіčάтέ τàв !!! ₣σúŋδ ą φѓοƒĩļé ẃϊţħ äй ïηνàĺìď "backgroundImage". Đēƒãųŀŧϊпğ ťнªт φѓőƒĭļè το нªνе πō ьąçќġгθúпδ ιмãġė. Маĸē śμѓē ŧћäţ ẁђēή šêťτϊлġ å "backgroundImage", ţĥě νаłųё ïŝ ά νάľîď ƒĩŀê φąťħ ţŏ άń ΐмąġė. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! ! @@ -462,7 +462,7 @@ Åвōύţ ! - Ѕеηð ₣ę℮đвäçк !!! + Ѕеηð ƒę℮đвäçк !!! ΦΚ @@ -472,11 +472,11 @@ This is the heading for a version number label - Ġеťтΐñĝ Ѕτдŗτęď !!! ! + Ġеťтΐñĝ ѕτдŗτęď !!! ! A hyperlink name for a guide on how to get started using Terminal - Ѕοџŗсė Çŏđе !!! + Ѕοџŗсė ¢ŏđе !!! A hyperlink name for the Terminal's documentation @@ -484,15 +484,15 @@ A hyperlink name for user documentation - Ŗеľ℮àşε Ŋòτéš !!! + Ŗеľ℮àşε πòτéš !!! A hyperlink name for the Terminal's release notes - Ρґїνãсÿ Рöĺĩςỳ !!! ! + Ρґїνãсÿ ρöĺĩςỳ !!! ! A hyperlink name for the Terminal's privacy policy - Ţћĩřð-Ρářŧγ ∏οŧīĉęŝ !!! !!! + Ţћĩřð-Ρářŧγ ñοŧīĉęŝ !!! !!! A hyperlink name for the Terminal's third-party notices @@ -579,10 +579,10 @@ ₣āіľ℮ď рàгśīпģ ¢бммäⁿδ ĺīñè: !!! !!! !! - Ćσmmăηδ Рάŀĕтţ℮ !!! ! + Ćσmmăηδ ράŀĕтţ℮ !!! ! - Τăь Ѕωîťςћêг !!! + Τăь ѕωîťςћêг !!! Ţýρё ă тăъ пâmě... !!! !! @@ -731,10 +731,10 @@ Μą×ìmϊżé !! - Ŕèšŧòяё Ðǿẃи !!! + Ŕèšŧòяё ďǿẃи !!! - Ċòмmāńδ Рªľėτťë !!! ! + Ċòмmāńδ рªľėτťë !!! ! ₣ôćűŝ Ţеґмĭйâŀ !!! ! @@ -754,7 +754,7 @@ Ŝρℓΐŧ ŧнė ẁίňďõŵ άпδ ŝţâґţ ίń ģįνëʼn δϊгέ¢ŧøяў !!! !!! !!! !!! ! - Ė×φōŗŧ Ţєхŧ !!! + Ė×φōŗŧ ţєхŧ !!! ₣ăìľεď ťθ эхроґт ţеґmίñдļ ¢ōйт℮лť !!! !!! !!! @@ -766,7 +766,7 @@ ₣ìпđ ! - Ρĺáīň Тěхт !!! + Ρĺáīň тěхт !!! Ťéямїлâŧîόň ь℮ћäνįőř čªή вĕ ċοñƒĩġџřèδ įŋ ăδνåл¢êð ряòƒιļє şėŧтіиĝś. !!! !!! !!! !!! !!! !!! !! @@ -775,7 +775,7 @@ Ďόń'ţ šħόω ãĝάϊл !!! ! - Ţђіś Тĕřмїηǻℓ шĩⁿðöŵ ïѕ ѓüñňĩñģ ãŝ Ãðmĭⁿ !!! !!! !!! !!! + Ţђіś Тĕřмїηǻℓ шĩⁿðöŵ ïѕ ѓüñňĩñģ ãŝ áðmĭⁿîşŧŕãţθŕ !!! !!! !!! !!! !! Ѕũğġεšтįóпş ƒōцʼnđ: {0} !!! !!! @@ -835,10 +835,10 @@ Ĉłоśэ ťĥìŝ ţªъ !!! ! - Ёмφţγ... !! + Ёмφţγ ! - Ĉĺοŝе Ρаиę !!! + Ĉĺοŝе φаиę !!! Çĺόś℮ τнĕ ă¢τίν℮ рáлĕ ιƒ mϋŀţїрĺë φàńęś άŗє рřęšеńт !!! !!! !!! !!! !!! @@ -848,13 +848,13 @@ Text used to identify the reset button - Мόνз Ţǻь ŧö П℮ω Щĭŋδōώ !!! !!! + Мόνз ţǻь ŧö ʼn℮ω ώĭŋδōώ !!! !!! Мøνëŝ ŧªъ ŧǿ ã пεẃ шίŋđоẁ !!! !!! ! - Ŕμŋ ąś Āďmįиíšťґąţőя !!! !!! + Ŕμŋ ąś åďmįиíšťґąţőя !!! !!! This text is displayed on context menu for profile entries in add new tab button. @@ -893,7 +893,7 @@ Ĩƒ šęţ, ŧнĕ ¢ömmдлδ ŵîĺł ьέ åφрєйδĕđ τσ ŧђė рřŏƒїłє'ş đзƒªūľţ ¢οmмăńδ іñѕţέáđ øƒ ѓēρļąċĭлĝ їţ. !!! !!! !!! !!! !!! !!! !!! !!! !!! ! - Γēѕŧâяŧ Ĉǿńńēčťїöл !!! !! + Γēѕŧâяŧ ćǿńńēčťїöл !!! !! Γėşťáгţ ŧħ℮ ãčтĩνέ ρăйё сǿηńëςтιóņ !!! !!! !!! ! diff --git a/src/cascadia/TerminalApp/Resources/qps-ploca/Resources.resw b/src/cascadia/TerminalApp/Resources/qps-ploca/Resources.resw index 0e553d2fc3..67031f290f 100644 --- a/src/cascadia/TerminalApp/Resources/qps-ploca/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/qps-ploca/Resources.resw @@ -191,43 +191,43 @@ Мµļтíрłĕ φдпėŝ !!! ! - Ćļôŝέ... !! + Ćļôŝέ ! - Ċĺοşέ Ţаъş ťό ŧђé Яΐğђт !!! !!! + Ċĺοşέ ţаъş ťό ŧђé яΐğђт !!! !!! - Ćĺόѕ℮ Όтђèř Ŧâьś !!! ! + Ćĺόѕ℮ őтђèř ŧâьś !!! ! - Сĺôšę Ťăв !!! + Сĺôšę ťăв !!! - Ćŀöśё Раņé !!! + Ćŀöśё ρаņé !!! - Šрľīτ Τàв !!! + Šрľīτ τàв !!! - Šрŀіт Ρªňë !!! + Šрŀіт φªňë !!! - Ẅёв Şĕаŕčĥ !!! + Ẅёв şĕаŕčĥ !!! - Ċõŀόř... !! + Ċħāņğě τāв ςōĺöя !!! ! - Ċµѕťøм... !!! + Ċµѕťøм ! Яěšěŧ ! - Γεñамē Ťãв !!! + Γεñамē ťãв !!! - Ďϋφľіčάтέ Τàв !!! + Ďϋφľіčάтέ τàв !!! ₣σúŋδ ą φѓοƒĩļé ẃϊţħ äй ïηνàĺìď "backgroundImage". Đēƒãųŀŧϊпğ ťнªт φѓőƒĭļè το нªνе πō ьąçќġгθúпδ ιмãġė. Маĸē śμѓē ŧћäţ ẁђēή šêťτϊлġ å "backgroundImage", ţĥě νаłųё ïŝ ά νάľîď ƒĩŀê φąťħ ţŏ άń ΐмąġė. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! ! @@ -462,7 +462,7 @@ Åвōύţ ! - Ѕеηð ₣ę℮đвäçк !!! + Ѕеηð ƒę℮đвäçк !!! ΦΚ @@ -472,11 +472,11 @@ This is the heading for a version number label - Ġеťтΐñĝ Ѕτдŗτęď !!! ! + Ġеťтΐñĝ ѕτдŗτęď !!! ! A hyperlink name for a guide on how to get started using Terminal - Ѕοџŗсė Çŏđе !!! + Ѕοџŗсė ¢ŏđе !!! A hyperlink name for the Terminal's documentation @@ -484,15 +484,15 @@ A hyperlink name for user documentation - Ŗеľ℮àşε Ŋòτéš !!! + Ŗеľ℮àşε πòτéš !!! A hyperlink name for the Terminal's release notes - Ρґїνãсÿ Рöĺĩςỳ !!! ! + Ρґїνãсÿ ρöĺĩςỳ !!! ! A hyperlink name for the Terminal's privacy policy - Ţћĩřð-Ρářŧγ ∏οŧīĉęŝ !!! !!! + Ţћĩřð-Ρářŧγ ñοŧīĉęŝ !!! !!! A hyperlink name for the Terminal's third-party notices @@ -579,10 +579,10 @@ ₣āіľ℮ď рàгśīпģ ¢бммäⁿδ ĺīñè: !!! !!! !! - Ćσmmăηδ Рάŀĕтţ℮ !!! ! + Ćσmmăηδ ράŀĕтţ℮ !!! ! - Τăь Ѕωîťςћêг !!! + Τăь ѕωîťςћêг !!! Ţýρё ă тăъ пâmě... !!! !! @@ -731,10 +731,10 @@ Μą×ìmϊżé !! - Ŕèšŧòяё Ðǿẃи !!! + Ŕèšŧòяё ďǿẃи !!! - Ċòмmāńδ Рªľėτťë !!! ! + Ċòмmāńδ рªľėτťë !!! ! ₣ôćűŝ Ţеґмĭйâŀ !!! ! @@ -754,7 +754,7 @@ Ŝρℓΐŧ ŧнė ẁίňďõŵ άпδ ŝţâґţ ίń ģįνëʼn δϊгέ¢ŧøяў !!! !!! !!! !!! ! - Ė×φōŗŧ Ţєхŧ !!! + Ė×φōŗŧ ţєхŧ !!! ₣ăìľεď ťθ эхроґт ţеґmίñдļ ¢ōйт℮лť !!! !!! !!! @@ -766,7 +766,7 @@ ₣ìпđ ! - Ρĺáīň Тěхт !!! + Ρĺáīň тěхт !!! Ťéямїлâŧîόň ь℮ћäνįőř čªή вĕ ċοñƒĩġџřèδ įŋ ăδνåл¢êð ряòƒιļє şėŧтіиĝś. !!! !!! !!! !!! !!! !!! !! @@ -775,7 +775,7 @@ Ďόń'ţ šħόω ãĝάϊл !!! ! - Ţђіś Тĕřмїηǻℓ шĩⁿðöŵ ïѕ ѓüñňĩñģ ãŝ Ãðmĭⁿ !!! !!! !!! !!! + Ţђіś Тĕřмїηǻℓ шĩⁿðöŵ ïѕ ѓüñňĩñģ ãŝ áðmĭⁿîşŧŕãţθŕ !!! !!! !!! !!! !! Ѕũğġεšтįóпş ƒōцʼnđ: {0} !!! !!! @@ -835,10 +835,10 @@ Ĉłоśэ ťĥìŝ ţªъ !!! ! - Ёмφţγ... !! + Ёмφţγ ! - Ĉĺοŝе Ρаиę !!! + Ĉĺοŝе φаиę !!! Çĺόś℮ τнĕ ă¢τίν℮ рáлĕ ιƒ mϋŀţїрĺë φàńęś άŗє рřęšеńт !!! !!! !!! !!! !!! @@ -848,13 +848,13 @@ Text used to identify the reset button - Мόνз Ţǻь ŧö П℮ω Щĭŋδōώ !!! !!! + Мόνз ţǻь ŧö ʼn℮ω ώĭŋδōώ !!! !!! Мøνëŝ ŧªъ ŧǿ ã пεẃ шίŋđоẁ !!! !!! ! - Ŕμŋ ąś Āďmįиíšťґąţőя !!! !!! + Ŕμŋ ąś åďmįиíšťґąţőя !!! !!! This text is displayed on context menu for profile entries in add new tab button. @@ -893,7 +893,7 @@ Ĩƒ šęţ, ŧнĕ ¢ömmдлδ ŵîĺł ьέ åφрєйδĕđ τσ ŧђė рřŏƒїłє'ş đзƒªūľţ ¢οmмăńδ іñѕţέáđ øƒ ѓēρļąċĭлĝ їţ. !!! !!! !!! !!! !!! !!! !!! !!! !!! ! - Γēѕŧâяŧ Ĉǿńńēčťїöл !!! !! + Γēѕŧâяŧ ćǿńńēčťїöл !!! !! Γėşťáгţ ŧħ℮ ãčтĩνέ ρăйё сǿηńëςтιóņ !!! !!! !!! ! diff --git a/src/cascadia/TerminalApp/Resources/qps-plocm/Resources.resw b/src/cascadia/TerminalApp/Resources/qps-plocm/Resources.resw index 0e553d2fc3..67031f290f 100644 --- a/src/cascadia/TerminalApp/Resources/qps-plocm/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/qps-plocm/Resources.resw @@ -191,43 +191,43 @@ Мµļтíрłĕ φдпėŝ !!! ! - Ćļôŝέ... !! + Ćļôŝέ ! - Ċĺοşέ Ţаъş ťό ŧђé Яΐğђт !!! !!! + Ċĺοşέ ţаъş ťό ŧђé яΐğђт !!! !!! - Ćĺόѕ℮ Όтђèř Ŧâьś !!! ! + Ćĺόѕ℮ őтђèř ŧâьś !!! ! - Сĺôšę Ťăв !!! + Сĺôšę ťăв !!! - Ćŀöśё Раņé !!! + Ćŀöśё ρаņé !!! - Šрľīτ Τàв !!! + Šрľīτ τàв !!! - Šрŀіт Ρªňë !!! + Šрŀіт φªňë !!! - Ẅёв Şĕаŕčĥ !!! + Ẅёв şĕаŕčĥ !!! - Ċõŀόř... !! + Ċħāņğě τāв ςōĺöя !!! ! - Ċµѕťøм... !!! + Ċµѕťøм ! Яěšěŧ ! - Γεñамē Ťãв !!! + Γεñамē ťãв !!! - Ďϋφľіčάтέ Τàв !!! + Ďϋφľіčάтέ τàв !!! ₣σúŋδ ą φѓοƒĩļé ẃϊţħ äй ïηνàĺìď "backgroundImage". Đēƒãųŀŧϊпğ ťнªт φѓőƒĭļè το нªνе πō ьąçќġгθúпδ ιмãġė. Маĸē śμѓē ŧћäţ ẁђēή šêťτϊлġ å "backgroundImage", ţĥě νаłųё ïŝ ά νάľîď ƒĩŀê φąťħ ţŏ άń ΐмąġė. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! ! @@ -462,7 +462,7 @@ Åвōύţ ! - Ѕеηð ₣ę℮đвäçк !!! + Ѕеηð ƒę℮đвäçк !!! ΦΚ @@ -472,11 +472,11 @@ This is the heading for a version number label - Ġеťтΐñĝ Ѕτдŗτęď !!! ! + Ġеťтΐñĝ ѕτдŗτęď !!! ! A hyperlink name for a guide on how to get started using Terminal - Ѕοџŗсė Çŏđе !!! + Ѕοџŗсė ¢ŏđе !!! A hyperlink name for the Terminal's documentation @@ -484,15 +484,15 @@ A hyperlink name for user documentation - Ŗеľ℮àşε Ŋòτéš !!! + Ŗеľ℮àşε πòτéš !!! A hyperlink name for the Terminal's release notes - Ρґїνãсÿ Рöĺĩςỳ !!! ! + Ρґїνãсÿ ρöĺĩςỳ !!! ! A hyperlink name for the Terminal's privacy policy - Ţћĩřð-Ρářŧγ ∏οŧīĉęŝ !!! !!! + Ţћĩřð-Ρářŧγ ñοŧīĉęŝ !!! !!! A hyperlink name for the Terminal's third-party notices @@ -579,10 +579,10 @@ ₣āіľ℮ď рàгśīпģ ¢бммäⁿδ ĺīñè: !!! !!! !! - Ćσmmăηδ Рάŀĕтţ℮ !!! ! + Ćσmmăηδ ράŀĕтţ℮ !!! ! - Τăь Ѕωîťςћêг !!! + Τăь ѕωîťςћêг !!! Ţýρё ă тăъ пâmě... !!! !! @@ -731,10 +731,10 @@ Μą×ìmϊżé !! - Ŕèšŧòяё Ðǿẃи !!! + Ŕèšŧòяё ďǿẃи !!! - Ċòмmāńδ Рªľėτťë !!! ! + Ċòмmāńδ рªľėτťë !!! ! ₣ôćűŝ Ţеґмĭйâŀ !!! ! @@ -754,7 +754,7 @@ Ŝρℓΐŧ ŧнė ẁίňďõŵ άпδ ŝţâґţ ίń ģįνëʼn δϊгέ¢ŧøяў !!! !!! !!! !!! ! - Ė×φōŗŧ Ţєхŧ !!! + Ė×φōŗŧ ţєхŧ !!! ₣ăìľεď ťθ эхроґт ţеґmίñдļ ¢ōйт℮лť !!! !!! !!! @@ -766,7 +766,7 @@ ₣ìпđ ! - Ρĺáīň Тěхт !!! + Ρĺáīň тěхт !!! Ťéямїлâŧîόň ь℮ћäνįőř čªή вĕ ċοñƒĩġџřèδ įŋ ăδνåл¢êð ряòƒιļє şėŧтіиĝś. !!! !!! !!! !!! !!! !!! !! @@ -775,7 +775,7 @@ Ďόń'ţ šħόω ãĝάϊл !!! ! - Ţђіś Тĕřмїηǻℓ шĩⁿðöŵ ïѕ ѓüñňĩñģ ãŝ Ãðmĭⁿ !!! !!! !!! !!! + Ţђіś Тĕřмїηǻℓ шĩⁿðöŵ ïѕ ѓüñňĩñģ ãŝ áðmĭⁿîşŧŕãţθŕ !!! !!! !!! !!! !! Ѕũğġεšтįóпş ƒōцʼnđ: {0} !!! !!! @@ -835,10 +835,10 @@ Ĉłоśэ ťĥìŝ ţªъ !!! ! - Ёмφţγ... !! + Ёмφţγ ! - Ĉĺοŝе Ρаиę !!! + Ĉĺοŝе φаиę !!! Çĺόś℮ τнĕ ă¢τίν℮ рáлĕ ιƒ mϋŀţїрĺë φàńęś άŗє рřęšеńт !!! !!! !!! !!! !!! @@ -848,13 +848,13 @@ Text used to identify the reset button - Мόνз Ţǻь ŧö П℮ω Щĭŋδōώ !!! !!! + Мόνз ţǻь ŧö ʼn℮ω ώĭŋδōώ !!! !!! Мøνëŝ ŧªъ ŧǿ ã пεẃ шίŋđоẁ !!! !!! ! - Ŕμŋ ąś Āďmįиíšťґąţőя !!! !!! + Ŕμŋ ąś åďmįиíšťґąţőя !!! !!! This text is displayed on context menu for profile entries in add new tab button. @@ -893,7 +893,7 @@ Ĩƒ šęţ, ŧнĕ ¢ömmдлδ ŵîĺł ьέ åφрєйδĕđ τσ ŧђė рřŏƒїłє'ş đзƒªūľţ ¢οmмăńδ іñѕţέáđ øƒ ѓēρļąċĭлĝ їţ. !!! !!! !!! !!! !!! !!! !!! !!! !!! ! - Γēѕŧâяŧ Ĉǿńńēčťїöл !!! !! + Γēѕŧâяŧ ćǿńńēčťїöл !!! !! Γėşťáгţ ŧħ℮ ãčтĩνέ ρăйё сǿηńëςтιóņ !!! !!! !!! ! diff --git a/src/cascadia/TerminalApp/Resources/ru-RU/Resources.resw b/src/cascadia/TerminalApp/Resources/ru-RU/Resources.resw index 0a74add4c3..b4f32ce009 100644 --- a/src/cascadia/TerminalApp/Resources/ru-RU/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/ru-RU/Resources.resw @@ -193,7 +193,7 @@ Закрыть вкладки справа - Закрыть другие вкладки + Закрыть остальные вкладки Закрыть вкладку @@ -211,10 +211,10 @@ Поиск в Интернете - Цвет... + Изменить цвет вкладки - Настраиваемый... + Настраиваемый Сбросить @@ -464,7 +464,7 @@ This is the heading for a version number label - Начало работы + Приступая к работе A hyperlink name for a guide on how to get started using Terminal @@ -484,7 +484,7 @@ A hyperlink name for the Terminal's privacy policy - Уведомления сторонних производителей + Уведомления третьих лиц A hyperlink name for the Terminal's third-party notices @@ -723,7 +723,7 @@ Развернуть - Восстановить размер + Кнопка "Свернуть в окно" Палитра команд @@ -827,7 +827,7 @@ Закрыть эту вкладку - Пусто... + Очистить Закрыть панель diff --git a/src/cascadia/TerminalApp/Resources/zh-CN/Resources.resw b/src/cascadia/TerminalApp/Resources/zh-CN/Resources.resw index 78135ba31a..e535d5a293 100644 --- a/src/cascadia/TerminalApp/Resources/zh-CN/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/zh-CN/Resources.resw @@ -187,7 +187,7 @@ 多个窗格 - 关闭… + 关闭 关闭右侧标签页 @@ -208,13 +208,13 @@ 拆分窗格 - Web 搜索 + 网络搜索 - 颜色... + 更改选项卡颜色 - 自定义... + 自定义 重置 @@ -223,7 +223,7 @@ 重命名选项卡 - 复制选项卡 + 复制标签页 找到一个具有无效 "backgroundImage" 的配置文件。将该配置文件设置为默认设置为不包含背景图像。请确保在设置 "backgroundImage" 时,该值是指向图像的有效文件路径。 @@ -480,7 +480,7 @@ A hyperlink name for the Terminal's release notes - 隐私策略 + 隐私政策 A hyperlink name for the Terminal's privacy policy @@ -827,7 +827,7 @@ 关闭此选项卡 - 空白... + 关闭窗格 diff --git a/src/cascadia/TerminalApp/Resources/zh-TW/Resources.resw b/src/cascadia/TerminalApp/Resources/zh-TW/Resources.resw index 5a7e89a671..49ed1b10b9 100644 --- a/src/cascadia/TerminalApp/Resources/zh-TW/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/zh-TW/Resources.resw @@ -187,7 +187,7 @@ 多個窗格 - 關閉... + 關閉 關閉右側的索引標籤 @@ -202,19 +202,19 @@ 關閉窗格 - 分割 Tab + 分割索引標籤 分割窗格 - Web 搜尋 + 網頁搜尋 - 色彩... + 變更索引標籤色彩 - 自訂...​​ + 自訂 重設 @@ -468,7 +468,7 @@ A hyperlink name for a guide on how to get started using Terminal - 原始程式碼 + 原始碼 A hyperlink name for the Terminal's documentation @@ -480,11 +480,11 @@ A hyperlink name for the Terminal's release notes - 隱私權原則 + 隱私權政策 A hyperlink name for the Terminal's privacy policy - 第三方聲明 + 第三方注意事項 A hyperlink name for the Terminal's third-party notices @@ -827,7 +827,7 @@ 關閉此索引標籤 - 空白... + 清空 關閉窗格 diff --git a/src/cascadia/TerminalSettingsModel/Resources/de-DE/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/de-DE/Resources.resw index 1b088d5705..8ab253b98c 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/de-DE/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/de-DE/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Farbschema auswählen... + Farbschema auswählen - Neue Registerkarte... + Neue Registerkarte - Bereich teilen... + Bereich teilen Terminal (portierbar) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Die Farbe der Registerkarte festlegen... + Registerkartenfarbe festlegen Einfügen @@ -347,7 +347,7 @@ Den Titel der Registerkarte zurücksetzen - Registerkartentitel umbenennen... + Registerkartentitel umbenennen Bereich skalieren @@ -441,7 +441,7 @@ Zur letzten Registerkarte wechseln - Registerkarte suchen... + Registerkarte suchen Modus "Immer im Vordergrund" umschalten @@ -450,10 +450,10 @@ Befehlspalette ein/aus - Zuletzt verwendete Befehle... + Zuletzt verwendete Befehle - Vorschläge öffnen... + Vorschläge öffnen Befehlspalette im Befehlszeilenmodus ein/aus @@ -505,7 +505,7 @@ Debugger unterbrechen - Einstellungen öffnen... + Einstellungen öffnen Fenster in „{0}“ umbenennen @@ -515,7 +515,7 @@ Fensternamen zurücksetzen - Fenster umbenennen... + Fenster umbenennen Aktuelles Arbeitsverzeichnis des Terminals anzeigen @@ -566,7 +566,7 @@ Terminal beenden - Hintergrunddeckkraft festlegen... + Hintergrunddeckkraft festlegen Hintergrunddeckkraft um {0} % erhöhen diff --git a/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw index 9d9ef4b93f..c3c94c309a 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Seleccionar combinación de colores... + Seleccionar combinación de colores - Nueva pestaña... + Nueva pestaña - Panel dividido... + Panel dividido Terminal (portátil) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Establecer el color de la pestaña... + Establecer el color de la pestaña Pegar @@ -347,7 +347,7 @@ Restablecer título de la pestaña - Cambiar nombre de título de la pestaña... + Cambiar título de la pestaña Cambiar el tamaño del panel @@ -441,7 +441,7 @@ Cambiar a la última pestaña - Buscar pestañas... + Buscar pestaña Alternar siempre en el modo superior @@ -450,10 +450,10 @@ Alternar paleta de comandos - Comandos recientes... + Comandos recientes - Abrir sugerencias... + Abrir sugerencias Alternar paleta de comandos en modo de línea de comandos @@ -505,7 +505,7 @@ Interrumpir en el depurador - Abrir configuración... + Abrir configuración Cambiar el nombre de la ventana por "{0}" @@ -566,7 +566,7 @@ Salir del terminal - Establecer la opacidad del fondo... + Establecer la opacidad del fondo Aumentar la opacidad del fondo en un {0} % diff --git a/src/cascadia/TerminalSettingsModel/Resources/fr-FR/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/fr-FR/Resources.resw index ce62381140..21fe9bcf22 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/fr-FR/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/fr-FR/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Sélectionner le modèle de couleurs... + Sélectionner un modèle de couleurs - Nouvel onglet... + Nouvel onglet - Fractionner le volet... + Fractionner le volet Terminal (portable) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Définir la couleur de l’onglet... + Définir la couleur de l’onglet Coller @@ -347,7 +347,7 @@ Rétablir le titre de l’onglet - Renommer le titre de l'onglet... + Renommer le titre de l'onglet Redimensionner le volet @@ -441,7 +441,7 @@ Basculer vers le dernier onglet - Recherche de l’onglet... + Recherche de l’onglet Activer le mode Toujours visible @@ -450,10 +450,10 @@ Activer/désactiver la palette de commandes - Commandes récentes... + Commandes récentes - Suggestions ouvertes... + Ouvrir les suggestions Basculer la palette de commandes en mode ligne de commande @@ -505,7 +505,7 @@ Accès dans le débogueur - Ouvrir les paramètres... + Ouvrir les paramètres Renommer la fenêtre en « {0} » @@ -515,7 +515,7 @@ Réinitialiser le nom de la fenêtre - Renommer la fenêtre... + Fenêtre pour renommer Afficher le répertoire de travail actuel du terminal @@ -566,7 +566,7 @@ Quitter le terminal - Définir l’opacité de l’arrière-plan... + Définir l’opacité de l’arrière-plan Augmenter l’opacité de l’arrière-plan de {0} % diff --git a/src/cascadia/TerminalSettingsModel/Resources/it-IT/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/it-IT/Resources.resw index 48ba557745..68d19388ce 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/it-IT/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Seleziona combinazione colori... + Seleziona combinazione colori - Nuova scheda... + Nuova scheda - Suddividi riquadro... + Suddividi riquadro Terminale (portatile) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Imposta il colore della scheda... + Imposta il colore della scheda Incolla @@ -347,7 +347,7 @@ Reimposta il titolo della scheda - Rinomina il titolo della scheda ... + Rinomina il titolo della scheda Ridimensiona riquadro @@ -441,7 +441,7 @@ Passa all'ultima scheda - Cerca scheda... + Cerca scheda Modalità attiva/disattiva sempre in alto @@ -450,10 +450,10 @@ Attiva/disattiva riquadro dei comandi - Comandi recenti... + Comandi recenti - Apri suggerimenti... + Apri i suggerimenti Attiva/disattiva il pannello dei comandi in modalità riga di comando @@ -505,7 +505,7 @@ Interrompi il debugger - Apri impostazioni... + Apri impostazioni Rinomina finestra in "{0}" @@ -515,7 +515,7 @@ Reimposta nome finestra - Rinomina finestra... + Rinomina finestra Visualizza la directory di lavoro corrente del terminale @@ -566,7 +566,7 @@ Esci da Terminale - Imposta l'opacità di sfondo... + Imposta l'opacità di sfondo Aumenta l'opacità di sfondo del {0}% diff --git a/src/cascadia/TerminalSettingsModel/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/ja-JP/Resources.resw index 173663fc99..3a86cbc028 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/ja-JP/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 配色パターンの選択... + 配色パターンの選択 - 新しいタブ... + 新しいタブ - ウィンドウを分割します... + ウィンドウを分割する ターミナル (ポータブル) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - タブの色を設定する... + タブの色の設定 貼り付け @@ -347,7 +347,7 @@ タブのタイトルをリセット - タブ名を変更する + タブ タイトル名の変更 ウィンドウのサイズの変更する @@ -441,7 +441,7 @@ 最後のタブに切り替える - タブを検索します... + タブの検索 常に手前に表示するモードに切り替える @@ -450,10 +450,10 @@ コマンド パレットに切り替える - 最近使ったコマンド... + 最近使ったコマンド - おすすめを開く... + 候補を開く コマンド ライン モードでコマンド パレットを切り替えます @@ -505,7 +505,7 @@ デバッガーに挿入します - 設定を開く... + 設定を開く ウィンドウの名前を "{0}" に変更する @@ -566,7 +566,7 @@ ターミナルの終了 - 背景の不透明度を設定... + 背景の不透明度の設定 背景の不透明度を {0}% 上げる diff --git a/src/cascadia/TerminalSettingsModel/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/ko-KR/Resources.resw index 90be00c1d2..b776a0cffd 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/ko-KR/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 색 구성표 선택... + 색 구성표 선택 - 새 탭... + 새 탭 - 분할 창... + 분할 창 터미널(이식 가능) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - 탭 색 설정... + 탭 색 설정 붙여넣기 @@ -347,7 +347,7 @@ 탭 제목 재설정 - 탭 이름 바꾸기... + 탭 제목 이름 바꾸기 창 크기 조정 @@ -450,10 +450,10 @@ 토글 명령 팔레트 - 최근 명령... + 최근 명령 - 제안 열기... + 제안 사항 열기 명령줄 모드에서 명령 팔레트 설정/해제 @@ -505,7 +505,7 @@ 디버거로 나누기 - 설정 열기... + 설정 열기 "{0}"(으)로 창 이름 바꾸기 @@ -566,7 +566,7 @@ 터미널 종료 - 배경 불투명도 설정... + 배경 불투명도 설정 배경 불투명도를 {0}% 증가 diff --git a/src/cascadia/TerminalSettingsModel/Resources/pt-BR/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/pt-BR/Resources.resw index aa4a632c36..cb4bd2648a 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/pt-BR/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/pt-BR/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Selecionar esquema de cores... + Selecionar esquema de cores - Nova Guia... + Nova guia - Dividir painel... + Dividir painel Terminal (Portátil) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Definir a cor da guia... + Definir a cor da guia Colar @@ -347,7 +347,7 @@ Redefinir título da guia - Renomear o título da guia... + Renomear título da guia Redimensionar painel @@ -441,7 +441,7 @@ Alternar para a última guia - Buscar guia + Pesquisar guia Alternar sempre no modo superior @@ -450,7 +450,7 @@ Ativar/desativar paleta de comandos - Comandos recentes... + Comandos recentes Abrir sugestões @@ -505,7 +505,7 @@ Invadir o depurador - Abrir as configurações... + Abrir configurações Renomear a janela para "{0}" @@ -566,7 +566,7 @@ Saia do Terminal - Definir a opacidade da tela de fundo + Definir opacidade da tela de fundo Aumentar a opacidade da tela de fundo em {0}% diff --git a/src/cascadia/TerminalSettingsModel/Resources/qps-ploc/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/qps-ploc/Resources.resw index d2eea06299..e93d8c85be 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/qps-ploc/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/qps-ploc/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Ŝêĺĕćţ ćŏĺбŕ ѕćнêmē... !!! !!! + Ŝêĺĕćţ ćŏĺбŕ ѕćнêmē !!! !!! - Ŋéŵ Тάъ... !!! + Ŋéŵ тάъ !! - Śрĺíŧ Рāлë... !!! + Śрĺíŧ ρāлë !!! Ţèѓmĩŋąĺ (Ρθŗτаьℓ℮) !!! !!! @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Şėŧ ŧнę ťáь ċσŀőґ... !!! !!! + Şėŧ ŧнę ťáь ċσŀőґ !!! !! Ρášτé ! @@ -347,7 +347,7 @@ Řėś℮ŧ тαв τįтℓę !!! ! - Гęйªm℮ тåъ ŧīτļĕ... !!! !!! + Гęйªm℮ тåъ ŧīτļĕ !!! ! Ґëśîžє рãле !!! @@ -441,7 +441,7 @@ Ѕẃітčћ ţõ ťħέ ľάšť ţаь !!! !!! - Ѕėàřĉħ ƒôґ ŧâь... !!! !! + Ѕėàřĉħ ƒôґ ŧâь !!! ! Ŧōĝġļě αŀώªÿŝ òⁿ тοр мöðέ !!! !!! ! @@ -450,10 +450,10 @@ Ţōĝğļė čσmmάήđ рåŀęŧŧз !!! !!! - Γë¢єйť ćøмmåηđŝ... !!! !! + Γë¢єйť ćøмmåηđŝ !!! ! - Ǿрέʼn şµġġеšŧìŏπѕ... !!! !!! + Ǿрέʼn şµġġеšŧìŏπѕ !!! ! Ťŏġĝļĕ çбmмáήď φàłėŧτĕ ίη ċσmмäпδ ℓìņє mόð℮ !!! !!! !!! !!! @@ -505,7 +505,7 @@ Вгεаĸ ĩňŧθ ŧђė đěвцģģέŕ !!! !!! - Öφéη ѕēττϊⁿğš... !!! ! + Öφéη ѕēττϊⁿğš !!! Γзňãмē шіňďоώ ŧō "{0}" !!! !!! @@ -515,7 +515,7 @@ Ŗεšзť ωĩйδōẁ ñâмé !!! !! - Ґёʼnάmë шϊйďθŵ... !!! ! + Ґёʼnάmë шϊйďθŵ !!! Ďіŝρłάỳ Τ℮ѓmìйаĺ'š čûŗяēʼnτ ώоřκìņĝ ďιяęĉтσґỳ !!! !!! !!! !!! ! @@ -566,7 +566,7 @@ Qυϊτ ŧħз Τέяmίŋăŀ !!! !! - Ѕěт ьªċķĝґøūņð óφǻĉїτў... !!! !!! ! + Ѕěт ьªċķĝґøūņð óφǻĉїτў !!! !!! Ĭñçŕ℮àŝę ваċкġřŏųлď ǿφāςíτу ьỳ {0}% !!! !!! !!! ! diff --git a/src/cascadia/TerminalSettingsModel/Resources/qps-ploca/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/qps-ploca/Resources.resw index d2eea06299..e93d8c85be 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/qps-ploca/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/qps-ploca/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Ŝêĺĕćţ ćŏĺбŕ ѕćнêmē... !!! !!! + Ŝêĺĕćţ ćŏĺбŕ ѕćнêmē !!! !!! - Ŋéŵ Тάъ... !!! + Ŋéŵ тάъ !! - Śрĺíŧ Рāлë... !!! + Śрĺíŧ ρāлë !!! Ţèѓmĩŋąĺ (Ρθŗτаьℓ℮) !!! !!! @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Şėŧ ŧнę ťáь ċσŀőґ... !!! !!! + Şėŧ ŧнę ťáь ċσŀőґ !!! !! Ρášτé ! @@ -347,7 +347,7 @@ Řėś℮ŧ тαв τįтℓę !!! ! - Гęйªm℮ тåъ ŧīτļĕ... !!! !!! + Гęйªm℮ тåъ ŧīτļĕ !!! ! Ґëśîžє рãле !!! @@ -441,7 +441,7 @@ Ѕẃітčћ ţõ ťħέ ľάšť ţаь !!! !!! - Ѕėàřĉħ ƒôґ ŧâь... !!! !! + Ѕėàřĉħ ƒôґ ŧâь !!! ! Ŧōĝġļě αŀώªÿŝ òⁿ тοр мöðέ !!! !!! ! @@ -450,10 +450,10 @@ Ţōĝğļė čσmmάήđ рåŀęŧŧз !!! !!! - Γë¢єйť ćøмmåηđŝ... !!! !! + Γë¢єйť ćøмmåηđŝ !!! ! - Ǿрέʼn şµġġеšŧìŏπѕ... !!! !!! + Ǿрέʼn şµġġеšŧìŏπѕ !!! ! Ťŏġĝļĕ çбmмáήď φàłėŧτĕ ίη ċσmмäпδ ℓìņє mόð℮ !!! !!! !!! !!! @@ -505,7 +505,7 @@ Вгεаĸ ĩňŧθ ŧђė đěвцģģέŕ !!! !!! - Öφéη ѕēττϊⁿğš... !!! ! + Öφéη ѕēττϊⁿğš !!! Γзňãмē шіňďоώ ŧō "{0}" !!! !!! @@ -515,7 +515,7 @@ Ŗεšзť ωĩйδōẁ ñâмé !!! !! - Ґёʼnάmë шϊйďθŵ... !!! ! + Ґёʼnάmë шϊйďθŵ !!! Ďіŝρłάỳ Τ℮ѓmìйаĺ'š čûŗяēʼnτ ώоřκìņĝ ďιяęĉтσґỳ !!! !!! !!! !!! ! @@ -566,7 +566,7 @@ Qυϊτ ŧħз Τέяmίŋăŀ !!! !! - Ѕěт ьªċķĝґøūņð óφǻĉїτў... !!! !!! ! + Ѕěт ьªċķĝґøūņð óφǻĉїτў !!! !!! Ĭñçŕ℮àŝę ваċкġřŏųлď ǿφāςíτу ьỳ {0}% !!! !!! !!! ! diff --git a/src/cascadia/TerminalSettingsModel/Resources/qps-plocm/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/qps-plocm/Resources.resw index d2eea06299..e93d8c85be 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/qps-plocm/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/qps-plocm/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Ŝêĺĕćţ ćŏĺбŕ ѕćнêmē... !!! !!! + Ŝêĺĕćţ ćŏĺбŕ ѕćнêmē !!! !!! - Ŋéŵ Тάъ... !!! + Ŋéŵ тάъ !! - Śрĺíŧ Рāлë... !!! + Śрĺíŧ ρāлë !!! Ţèѓmĩŋąĺ (Ρθŗτаьℓ℮) !!! !!! @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Şėŧ ŧнę ťáь ċσŀőґ... !!! !!! + Şėŧ ŧнę ťáь ċσŀőґ !!! !! Ρášτé ! @@ -347,7 +347,7 @@ Řėś℮ŧ тαв τįтℓę !!! ! - Гęйªm℮ тåъ ŧīτļĕ... !!! !!! + Гęйªm℮ тåъ ŧīτļĕ !!! ! Ґëśîžє рãле !!! @@ -441,7 +441,7 @@ Ѕẃітčћ ţõ ťħέ ľάšť ţаь !!! !!! - Ѕėàřĉħ ƒôґ ŧâь... !!! !! + Ѕėàřĉħ ƒôґ ŧâь !!! ! Ŧōĝġļě αŀώªÿŝ òⁿ тοр мöðέ !!! !!! ! @@ -450,10 +450,10 @@ Ţōĝğļė čσmmάήđ рåŀęŧŧз !!! !!! - Γë¢єйť ćøмmåηđŝ... !!! !! + Γë¢єйť ćøмmåηđŝ !!! ! - Ǿрέʼn şµġġеšŧìŏπѕ... !!! !!! + Ǿрέʼn şµġġеšŧìŏπѕ !!! ! Ťŏġĝļĕ çбmмáήď φàłėŧτĕ ίη ċσmмäпδ ℓìņє mόð℮ !!! !!! !!! !!! @@ -505,7 +505,7 @@ Вгεаĸ ĩňŧθ ŧђė đěвцģģέŕ !!! !!! - Öφéη ѕēττϊⁿğš... !!! ! + Öφéη ѕēττϊⁿğš !!! Γзňãмē шіňďоώ ŧō "{0}" !!! !!! @@ -515,7 +515,7 @@ Ŗεšзť ωĩйδōẁ ñâмé !!! !! - Ґёʼnάmë шϊйďθŵ... !!! ! + Ґёʼnάmë шϊйďθŵ !!! Ďіŝρłάỳ Τ℮ѓmìйаĺ'š čûŗяēʼnτ ώоřκìņĝ ďιяęĉтσґỳ !!! !!! !!! !!! ! @@ -566,7 +566,7 @@ Qυϊτ ŧħз Τέяmίŋăŀ !!! !! - Ѕěт ьªċķĝґøūņð óφǻĉїτў... !!! !!! ! + Ѕěт ьªċķĝґøūņð óφǻĉїτў !!! !!! Ĭñçŕ℮àŝę ваċкġřŏųлď ǿφāςíτу ьỳ {0}% !!! !!! !!! ! diff --git a/src/cascadia/TerminalSettingsModel/Resources/ru-RU/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/ru-RU/Resources.resw index d5e70d8194..1134777776 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/ru-RU/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/ru-RU/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Выбрать цветовую схему... + Выбрать цветовую схему - Новая вкладка... + Новая вкладка - Разделить область... + Разделить область Терминал (переносной) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - Задать цвет вкладки… + Задать цвет вкладки Вставить @@ -347,7 +347,7 @@ Сбросить заголовок вкладки - Переименовать заголовок вкладки... + Переименовать заголовок вкладки Изменить размер области @@ -441,7 +441,7 @@ Переключиться на последнюю вкладку - Поиск вкладки... + Поиск вкладки Режим "поверх других окон" @@ -450,10 +450,10 @@ Показать или скрыть палитру команд - Недавние команды... + Последние команды - Открыть предложения... + Открыть предложения Включить или отключить палитру команд в режиме командной строки @@ -505,7 +505,7 @@ Перейти в отладчик - Открыть настройки... + Открыть параметры Переименовать окно в "{0}" @@ -515,7 +515,7 @@ Сбросить имя окна - Переименовать окно... + Переименовать окно Отобразить текущий рабочий каталог Терминала @@ -566,7 +566,7 @@ Выйти из терминала - Установить непрозрачность фона... + Установить непрозрачность фона Увеличить непрозрачность фона на {0}% diff --git a/src/cascadia/TerminalSettingsModel/Resources/zh-CN/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/zh-CN/Resources.resw index 24746d831a..39c3259a3e 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/zh-CN/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/zh-CN/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 选择配色方案... + 选择配色方案 - 新选项卡... + 新建标签页 - 拆分窗格... + 拆分窗格 终端 (便携) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - 设置选项卡颜色... + 设置选项卡颜色 粘贴 @@ -347,7 +347,7 @@ 重置标签页标题 - 正在重命名选项卡标题... + 重命名选项卡标题 调整窗格大小 @@ -441,7 +441,7 @@ 切换到最后一个选项卡 - 搜索选项卡... + 搜索选项卡 切换“总在最前面”模式 @@ -450,10 +450,10 @@ 切换命令面板 - 最近使用的命令... + 最近使用的命令 - 打开建议... + 打开建议 在命令行模式里切换命令面板 @@ -505,7 +505,7 @@ 强行进入调试程序 - 打开设置... + 打开设置 重命名窗口到 “{0}” @@ -515,7 +515,7 @@ 重置窗口名称 - 重命名窗口... + 重命名窗口 显示终端的当前工作目录 @@ -566,7 +566,7 @@ 退出终端 - 设置背景不透明度... + 设置背景不透明度 将背景不透明度增加 {0}% diff --git a/src/cascadia/TerminalSettingsModel/Resources/zh-TW/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/zh-TW/Resources.resw index 536d4b900b..946440cb0c 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/zh-TW/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/zh-TW/Resources.resw @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 選取彩色配置... + 選取彩色配置 - 新增索引標籤... + 新索引標籤 - 分割窗格... + 分割窗格 終端機 (可攜式) @@ -325,7 +325,7 @@ {Locked="JSON"}. "JSON" is the extension of the file that will be opened. - 設定索引標籤色彩... + 設定索引標籤色彩 貼上 @@ -347,7 +347,7 @@ 重設索引標籤標題 - 將索引標籤重新命名... + 重新命名索引標籤標題 調整頁面大小 @@ -441,7 +441,7 @@ 切換到最後一個索引標籤 - 搜尋索引標籤... + 搜尋索引標籤 啟用 [最上層顯示] 模式 @@ -450,10 +450,10 @@ 切換命令選擇區 - 最近使用的命令... + 最近使用的命令 - 開啟建議... + 開啟建議 在命令列模式中切換命令選擇區 @@ -505,7 +505,7 @@ 在偵錯工具中中斷 - 開啟設定... + 開啟設定 將視窗重新命名為「{0}」 @@ -515,7 +515,7 @@ 重設視窗名稱 - 重新命名視窗... + 重新命名視窗 顯示終端機目前的工作目錄 @@ -566,7 +566,7 @@ 結束 [終端機] - 設定背景不透明度... + 設定背景不透明度 增加背景不透明度為 {0}% From 11c4aa459d846f3ea9cd68cf5555cbe83cfef4ab Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Tue, 16 Apr 2024 21:06:59 +0530 Subject: [PATCH 02/53] Fix window style under minimized state (#17058) Closes: #13961 This PR changes the window styling we use under the minimized state. We now retain the active window styling so the content's height doesn't change when switching between minimized and maximized states. ## Validation Steps Performed - Open Terminal and go into Maximized mode. - Click on the Minimize button. - No `SizeChanged` event in `ControlCore`. - Click on the WT icon in the taskbar to restore it. - No `SizeChanged` event in `ControlCore`. --- src/cascadia/TerminalApp/MinMaxCloseControl.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalApp/MinMaxCloseControl.cpp b/src/cascadia/TerminalApp/MinMaxCloseControl.cpp index fac7d2b2b3..f4db97176e 100644 --- a/src/cascadia/TerminalApp/MinMaxCloseControl.cpp +++ b/src/cascadia/TerminalApp/MinMaxCloseControl.cpp @@ -116,6 +116,9 @@ namespace winrt::TerminalApp::implementation switch (visualState) { + case WindowVisualState::WindowVisualStateIconified: + // Iconified (aka minimized) state should preserve the active window styling + break; case WindowVisualState::WindowVisualStateMaximized: VisualStateManager::GoToState(MaximizeButton(), L"WindowStateMaximized", false); @@ -124,9 +127,7 @@ namespace winrt::TerminalApp::implementation CloseButton().Height(maximizedHeight); MaximizeToolTip().Text(RS_(L"WindowRestoreDownButtonToolTip")); break; - case WindowVisualState::WindowVisualStateNormal: - case WindowVisualState::WindowVisualStateIconified: default: VisualStateManager::GoToState(MaximizeButton(), L"WindowStateNormal", false); From d632c39cc3cef3075f09d881e03801518310d641 Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Wed, 17 Apr 2024 00:30:31 +0530 Subject: [PATCH 03/53] Fix wrong CommandPallete access (#17069) Closes: #17032 We were wrongly calling the Ctor of CommandPalette which led to the creation of an uninitialized winrt command palette object, and then OnCreateAutomationPeer() was called on that. This seems to be the cause of #17032. ## Validation Steps Performed - Open WT. - Try to tear off a tab out of the tab headers view. - WT doesn't crash. --- src/cascadia/TerminalApp/TabManagement.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index f2039d0043..3053d746df 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -881,7 +881,10 @@ namespace winrt::TerminalApp::implementation } } - CommandPalette().Visibility(Visibility::Collapsed); + if (const auto p = CommandPaletteElement()) + { + p.Visibility(Visibility::Collapsed); + } _UpdateTabView(); } From 90b8bb7c2d8932fc601aed9c26f13c77d5a1321d Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Wed, 17 Apr 2024 21:41:31 +0530 Subject: [PATCH 04/53] Improve Search Highlighting (#16611) ### The changeset involves: - Decoupling Selection and Search Highlighting code paths. - We no longer invalidate search highlights when: - Left-clicking on terminal - A new selection is made - Left-clicking on Search-box - Dispatching Find Next/Prev Match Action. (The search highlight was removed after pressing the first key of the Action's key combination) - And, anything that doesn't change buffer content, shouldn't invalidate the highlighted region (E.g. Cursor movement) - Highlighting foreground color is *actually* applied to the highlighted text. - Double-clicking on SearchBox no longer starts a text selection in the terminal. - Selected text is properly populated in the Search Box (#16355) Closes: #16355 ![image](https://github.com/microsoft/terminal/assets/55626797/8fd0345b-a8b2-4bc2-a25e-15d710127b63) ## Some Implementation Details ### Detecting text layout changes in the Control layer As Search Highlight regions need to be removed when new text is added, or the existing text is re-arranged due to window resize or similar events, a new event `TextLayoutUpdated` is added that notifies `CoreControl` of any text layout changes. The event is used to invalidate and remove all search highlight regions from the buffer (because the regions might not be _fresh_ anymore. The new event is raised when: 1. `AdaptDispatch` writes new text into the buffer. 2. MainBuffer is switched to AltBuffer or vice-versa. 3. The user resized the window. 4. Font size changed. 5. Zoom level changed. (Intensionally,) It's not raised when: 1. Buffer is scrolled. 2. The text cursor is moved. When `ControlCore` receives a `TextLayoutUpdated` event, it clears the Search Highlights in the *render data*, and raises an `UpdateSearchResults` event to notify `TermControl` to update the Search UI (`SearchBoxControl`). In the future, we can use `TextLayoutUpdated` event to start a new search which would refresh the results automatically after a slight delay (throttled). *VSCode already does this today*. ### How does AtlasEngine draw the highlighted regions? We follow a similar idea as for drawing the Selection region. When new regions are available, the old+new regions are marked invalidated. Later, a call to `_drawHighlighted()` is made at the end of `PaintBufferLine()` to override the highlighted regions' colors with highlight colors. The highlighting colors replace the buffer colors while search highlights are active. Note that to paint search highlights, we currently invalidate the row completely. This forces text shaping for the rows in the viewport that have at least one highlighted region. This is done to keep the (already lengthy) PR... simple. We could take advantage of the fact that only colors have changed and not the characters (or glyphs). I'm expecting that this could be improved like: 1. When search regions are added, we add the highlighting colors to the color bitmaps without causing text shaping. 2. When search regions are removed, we re-fill the color bitmaps with the original colors from the Buffer. ## Validation Steps: - New text, window resize, font size changes, zooming, and pasting content into the terminal removes search highlights. - highlighting colors override the foreground and background color of the text (in the rendered output). - Blinking, faded, reverse video, Intense text is highlighted as expected. --- src/buffer/out/search.cpp | 31 +-- src/buffer/out/search.h | 7 +- src/cascadia/TerminalControl/ControlCore.cpp | 90 ++++++-- src/cascadia/TerminalControl/ControlCore.h | 6 +- src/cascadia/TerminalControl/ControlCore.idl | 3 +- src/cascadia/TerminalControl/EventArgs.cpp | 2 +- src/cascadia/TerminalControl/EventArgs.h | 10 +- src/cascadia/TerminalControl/EventArgs.idl | 9 +- .../TerminalControl/SearchBoxControl.cpp | 35 ++- .../TerminalControl/SearchBoxControl.h | 3 + .../TerminalControl/SearchBoxControl.idl | 1 + .../TerminalControl/SearchBoxControl.xaml | 2 + src/cascadia/TerminalControl/TermControl.cpp | 60 +++-- src/cascadia/TerminalControl/TermControl.h | 5 +- src/cascadia/TerminalCore/Terminal.cpp | 29 +++ src/cascadia/TerminalCore/Terminal.hpp | 15 +- src/cascadia/TerminalCore/TerminalApi.cpp | 16 ++ .../TerminalCore/TerminalSelection.cpp | 35 --- .../TerminalCore/terminalrenderdata.cpp | 69 +++--- .../UnitTests_TerminalCore/ScrollTest.cpp | 1 - src/host/outputStream.cpp | 7 +- src/host/outputStream.hpp | 1 + src/host/renderData.cpp | 9 +- src/host/renderData.hpp | 4 +- src/host/ut_host/VtIoTests.cpp | 13 +- src/inc/til/point.h | 34 +++ src/inc/til/rect.h | 14 ++ src/interactivity/onecore/BgfxEngine.cpp | 5 - src/interactivity/onecore/BgfxEngine.hpp | 1 - src/renderer/atlas/AtlasEngine.api.cpp | 22 ++ src/renderer/atlas/AtlasEngine.cpp | 216 +++++++++++++----- src/renderer/atlas/AtlasEngine.h | 15 +- src/renderer/base/RenderEngineBase.cpp | 7 +- src/renderer/base/renderer.cpp | 75 +++--- src/renderer/base/renderer.hpp | 3 +- src/renderer/gdi/gdirenderer.hpp | 1 - src/renderer/gdi/paint.cpp | 7 - src/renderer/inc/IRenderData.hpp | 4 +- src/renderer/inc/IRenderEngine.hpp | 7 +- src/renderer/inc/RenderEngineBase.hpp | 3 +- src/renderer/uia/UiaRenderer.cpp | 5 - src/renderer/uia/UiaRenderer.hpp | 1 - src/renderer/vt/paint.cpp | 5 - src/renderer/vt/vtrenderer.hpp | 1 - src/renderer/wddmcon/WddmConRenderer.cpp | 5 - src/renderer/wddmcon/WddmConRenderer.hpp | 1 - src/terminal/adapter/ITerminalApi.hpp | 1 + src/terminal/adapter/adaptDispatch.cpp | 6 +- .../adapter/ut_adapter/adapterTest.cpp | 5 + 49 files changed, 571 insertions(+), 336 deletions(-) diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index fd8942e0ba..336fa2957f 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -8,7 +8,7 @@ using namespace Microsoft::Console::Types; -bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive) +bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive, std::vector* prevResults) { const auto& textBuffer = renderData.GetTextBuffer(); const auto lastMutationId = textBuffer.GetLastMutationId(); @@ -26,10 +26,13 @@ bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, c _caseInsensitive = caseInsensitive; _lastMutationId = lastMutationId; + if (prevResults) + { + *prevResults = std::move(_results); + } _results = textBuffer.SearchText(needle, caseInsensitive); _index = reverse ? gsl::narrow_cast(_results.size()) - 1 : 0; _step = reverse ? -1 : 1; - return true; } @@ -111,28 +114,6 @@ const til::point_span* Search::GetCurrent() const noexcept return nullptr; } -void Search::HighlightResults() const -{ - std::vector toSelect; - const auto& textBuffer = _renderData->GetTextBuffer(); - - for (const auto& r : _results) - { - const auto rbStart = textBuffer.BufferToScreenPosition(r.start); - const auto rbEnd = textBuffer.BufferToScreenPosition(r.end); - - til::inclusive_rect re; - re.top = rbStart.y; - re.bottom = rbEnd.y; - re.left = rbStart.x; - re.right = rbEnd.x; - - toSelect.emplace_back(re); - } - - _renderData->SelectSearchRegions(std::move(toSelect)); -} - // Routine Description: // - Takes the found word and selects it in the screen buffer @@ -161,4 +142,4 @@ const std::vector& Search::Results() const noexcept ptrdiff_t Search::CurrentMatch() const noexcept { return _index; -} +} \ No newline at end of file diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index a338f1272c..c115b8bdd5 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -25,7 +25,11 @@ class Search final public: Search() = default; - bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive); + bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, + const std::wstring_view& needle, + bool reverse, + bool caseInsensitive, + std::vector* prevResults = nullptr); void MoveToCurrentSelection(); void MoveToPoint(til::point anchor) noexcept; @@ -33,7 +37,6 @@ public: void FindNext() noexcept; const til::point_span* GetCurrent() const noexcept; - void HighlightResults() const; bool SelectCurrent() const; const std::vector& Results() const noexcept; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index c704b41231..1646140062 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -120,6 +120,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnShowWindowChanged = std::bind(&ControlCore::_terminalShowWindowChanged, this, std::placeholders::_1); _terminal->SetShowWindowCallback(pfnShowWindowChanged); + auto pfnTextLayoutUpdated = std::bind(&ControlCore::_terminalTextLayoutUpdated, this); + _terminal->SetTextLayoutUpdatedCallback(pfnTextLayoutUpdated); + auto pfnPlayMidiNote = std::bind(&ControlCore::_terminalPlayMidiNote, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); _terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote); @@ -1123,6 +1126,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (SUCCEEDED(hr) && hr != S_FALSE) { _connection.Resize(vp.Height(), vp.Width()); + + // let the UI know that the text layout has been updated + _terminal->NotifyTextLayoutUpdated(); } } @@ -1638,6 +1644,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation ShowWindowChanged.raise(*this, *showWindow); } + void ControlCore::_terminalTextLayoutUpdated() + { + ClearSearch(); + + // send an UpdateSearchResults event to the UI to put the Search UI into inactive state. + auto evArgs = winrt::make_self(); + evArgs->State(SearchState::Inactive); + UpdateSearchResults.raise(*this, *evArgs); + } + // Method Description: // - Plays a single MIDI note, blocking for the duration. // Arguments: @@ -1701,43 +1717,45 @@ namespace winrt::Microsoft::Terminal::Control::implementation { const auto lock = _terminal->LockForWriting(); - if (_searcher.ResetIfStale(*GetRenderData(), text, !goForward, !caseSensitive)) + std::vector oldResults; + if (_searcher.ResetIfStale(*GetRenderData(), text, !goForward, !caseSensitive, &oldResults)) { - _searcher.HighlightResults(); - _searcher.MoveToCurrentSelection(); _cachedSearchResultRows = {}; + if (SnapSearchResultToSelection()) + { + _searcher.MoveToCurrentSelection(); + SnapSearchResultToSelection(false); + } + + _terminal->SetSearchHighlights(_searcher.Results()); + _terminal->SetSearchHighlightFocused(_searcher.CurrentMatch()); } else { _searcher.FindNext(); + _terminal->SetSearchHighlightFocused(_searcher.CurrentMatch()); } + _renderer->TriggerSearchHighlight(oldResults); - const auto foundMatch = _searcher.SelectCurrent(); - auto foundResults = winrt::make_self(foundMatch); - if (foundMatch) + auto evArgs = winrt::make_self(); + if (!text.empty()) { - // this is used for search, - // DO NOT call _updateSelectionUI() here. - // We don't want to show the markers so manually tell it to clear it. - _terminal->SetBlockSelection(false); - UpdateSelectionMarkers.raise(*this, winrt::make(true)); - - foundResults->TotalMatches(gsl::narrow(_searcher.Results().size())); - foundResults->CurrentMatch(gsl::narrow(_searcher.CurrentMatch())); - - _terminal->AlwaysNotifyOnBufferRotation(true); + evArgs->State(SearchState::Active); + if (_searcher.GetCurrent()) + { + evArgs->FoundMatch(true); + evArgs->TotalMatches(gsl::narrow(_searcher.Results().size())); + evArgs->CurrentMatch(gsl::narrow(_searcher.CurrentMatch())); + } } - _renderer->TriggerSelection(); - // Raise a FoundMatch event, which the control will use to notify - // narrator if there was any results in the buffer - FoundMatch.raise(*this, *foundResults); + // Raise an UpdateSearchResults event, which the control will use to update the + // UI and notify the narrator about the updated search results in the buffer + UpdateSearchResults.raise(*this, *evArgs); } Windows::Foundation::Collections::IVector ControlCore::SearchResultRows() { - const auto lock = _terminal->LockForReading(); - if (!_cachedSearchResultRows) { auto results = std::vector(); @@ -1761,8 +1779,32 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::ClearSearch() { - _terminal->AlwaysNotifyOnBufferRotation(false); - _searcher = {}; + // nothing to clear if there's no results + if (_searcher.GetCurrent()) + { + const auto lock = _terminal->LockForWriting(); + _terminal->SetSearchHighlights({}); + _terminal->SetSearchHighlightFocused({}); + _renderer->TriggerSearchHighlight(_searcher.Results()); + _searcher = {}; + _cachedSearchResultRows = {}; + } + } + + // Method Description: + // - Tells ControlCore to snap the current search result index to currently + // selected text if the search was started using it. + void ControlCore::SnapSearchResultToSelection(bool shouldSnap) noexcept + { + _snapSearchResultToSelection = shouldSnap; + } + + // Method Description: + // - Returns true, if we should snap the current search result index to + // the currently selected text after a new search is started, else false. + bool ControlCore::SnapSearchResultToSelection() const noexcept + { + return _snapSearchResultToSelection; } void ControlCore::Close() diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 0faffb6953..31d8fe00c1 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -221,6 +221,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void ClearSearch(); + void SnapSearchResultToSelection(bool snap) noexcept; + bool SnapSearchResultToSelection() const noexcept; Windows::Foundation::Collections::IVector SearchResultRows(); @@ -281,7 +283,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event RaiseNotice; til::typed_event TransparencyChanged; til::typed_event<> ReceivedOutput; - til::typed_event FoundMatch; + til::typed_event UpdateSearchResults; til::typed_event ShowWindowChanged; til::typed_event UpdateSelectionMarkers; til::typed_event OpenHyperlink; @@ -321,6 +323,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer{ nullptr }; ::Search _searcher; + bool _snapSearchResultToSelection; winrt::handle _lastSwapChainHandle{ nullptr }; @@ -379,6 +382,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _terminalCursorPositionChanged(); void _terminalTaskbarProgressChanged(); void _terminalShowWindowChanged(bool showOrHide); + void _terminalTextLayoutUpdated(); void _terminalPlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 632d2d7375..91b58a41c7 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -131,6 +131,7 @@ namespace Microsoft.Terminal.Control void Search(String text, Boolean goForward, Boolean caseSensitive); void ClearSearch(); IVector SearchResultRows { get; }; + Boolean SnapSearchResultToSelection; Microsoft.Terminal.Core.Color ForegroundColor { get; }; Microsoft.Terminal.Core.Color BackgroundColor { get; }; @@ -179,7 +180,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler RaiseNotice; event Windows.Foundation.TypedEventHandler TransparencyChanged; event Windows.Foundation.TypedEventHandler ReceivedOutput; - event Windows.Foundation.TypedEventHandler FoundMatch; + event Windows.Foundation.TypedEventHandler UpdateSearchResults; event Windows.Foundation.TypedEventHandler UpdateSelectionMarkers; event Windows.Foundation.TypedEventHandler OpenHyperlink; event Windows.Foundation.TypedEventHandler CloseTerminalRequested; diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 730545d42a..4c4d169095 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -12,7 +12,7 @@ #include "ScrollPositionChangedArgs.g.cpp" #include "RendererWarningArgs.g.cpp" #include "TransparencyChangedEventArgs.g.cpp" -#include "FoundResultsArgs.g.cpp" +#include "UpdateSearchResultsEventArgs.g.cpp" #include "ShowWindowArgs.g.cpp" #include "UpdateSelectionMarkersEventArgs.g.cpp" #include "CompletionsChangedEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 33c215cb3b..247a8c988a 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -12,7 +12,7 @@ #include "ScrollPositionChangedArgs.g.h" #include "RendererWarningArgs.g.h" #include "TransparencyChangedEventArgs.g.h" -#include "FoundResultsArgs.g.h" +#include "UpdateSearchResultsEventArgs.g.h" #include "ShowWindowArgs.g.h" #include "UpdateSelectionMarkersEventArgs.g.h" #include "CompletionsChangedEventArgs.g.h" @@ -141,14 +141,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(double, Opacity); }; - struct FoundResultsArgs : public FoundResultsArgsT + struct UpdateSearchResultsEventArgs : public UpdateSearchResultsEventArgsT { public: - FoundResultsArgs(const bool foundMatch) : - _FoundMatch(foundMatch) - { - } + UpdateSearchResultsEventArgs() = default; + WINRT_PROPERTY(SearchState, State, SearchState::Inactive); WINRT_PROPERTY(bool, FoundMatch); WINRT_PROPERTY(int32_t, TotalMatches); WINRT_PROPERTY(int32_t, CurrentMatch); diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 958b3e15a6..19b31cd97d 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -78,8 +78,15 @@ namespace Microsoft.Terminal.Control Double Opacity { get; }; } - runtimeclass FoundResultsArgs + enum SearchState { + Inactive = 0, + Active = 1, + }; + + runtimeclass UpdateSearchResultsEventArgs + { + SearchState State { get; }; Boolean FoundMatch { get; }; Int32 TotalMatches { get; }; Int32 CurrentMatch { get; }; diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index dbb7bb7704..9ce4bba86a 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -26,18 +26,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation }); this->CharacterReceived({ this, &SearchBoxControl::_CharacterHandler }); this->KeyDown({ this, &SearchBoxControl::_KeyDownHandler }); - this->RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { - // Once the control is visible again we trigger SearchChanged event. - // We do this since we probably have a value from the previous search, - // and in such case logically the search changes from "nothing" to this value. - // A good example for SearchChanged event consumer is Terminal Control. - // Once the Search Box is open we want the Terminal Control - // to immediately perform the search with the value appearing in the box. - if (Visibility() == Visibility::Visible) - { - SearchChanged.raise(TextBox().Text(), _GoForward(), _CaseSensitive()); - } - }); _focusableElements.insert(TextBox()); _focusableElements.insert(CloseButton()); @@ -421,6 +409,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation SearchChanged.raise(TextBox().Text(), _GoForward(), _CaseSensitive()); } + // Method Description: + // - Handler for searchbox pointer-pressed. + // - Marks pointer events as handled so they don't bubble up to the terminal. + void SearchBoxControl::SearchBoxPointerPressedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e) + { + e.Handled(true); + } + + // Method Description: + // - Handler for searchbox pointer-released. + // - Marks pointer events as handled so they don't bubble up to the terminal. + void SearchBoxControl::SearchBoxPointerReleasedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e) + { + e.Handled(true); + } + // Method Description: // - Formats a status message representing the search state: // * "Searching" - if totalMatches is negative @@ -518,6 +522,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation StatusBox().Text(status); } + // Method Description: + // - Removes the status message in the status box. + void SearchBoxControl::ClearStatus() + { + StatusBox().Text(L""); + } + // Method Description: // - Enables / disables results navigation buttons // Arguments: diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 293f5d0270..53a738c399 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -39,6 +39,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); void SetStatus(int32_t totalMatches, int32_t currentMatch); + void ClearStatus(); bool NavigationEnabled(); void NavigationEnabled(bool enabled); @@ -48,6 +49,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); void CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void SearchBoxPointerPressedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void SearchBoxPointerReleasedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); til::event Search; til::event SearchChanged; diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl index 80cfcd87b3..42abd1fa1b 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.idl +++ b/src/cascadia/TerminalControl/SearchBoxControl.idl @@ -12,6 +12,7 @@ namespace Microsoft.Terminal.Control void PopulateTextbox(String text); Boolean ContainsFocus(); void SetStatus(Int32 totalMatches, Int32 currentMatch); + void ClearStatus(); Windows.Foundation.Rect ContentClipRect{ get; }; Double OpenAnimationStartPoint{ get; }; diff --git a/src/cascadia/TerminalControl/SearchBoxControl.xaml b/src/cascadia/TerminalControl/SearchBoxControl.xaml index d69bef8a1c..c17fcc3c01 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.xaml +++ b/src/cascadia/TerminalControl/SearchBoxControl.xaml @@ -181,6 +181,8 @@ BorderThickness="{ThemeResource FlyoutBorderThemeThickness}" CornerRadius="{ThemeResource OverlayCornerRadius}" Orientation="Horizontal" + PointerPressed="SearchBoxPointerPressedHandler" + PointerReleased="SearchBoxPointerReleasedHandler" Shadow="{StaticResource SharedShadow}" Translation="0,0,16"> diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 7c4a5b4763..96c78f0afc 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -85,7 +85,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _revokers.TransparencyChanged = _core.TransparencyChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreTransparencyChanged }); _revokers.RaiseNotice = _core.RaiseNotice(winrt::auto_revoke, { get_weak(), &TermControl::_coreRaisedNotice }); _revokers.HoveredHyperlinkChanged = _core.HoveredHyperlinkChanged(winrt::auto_revoke, { get_weak(), &TermControl::_hoveredHyperlinkChanged }); - _revokers.FoundMatch = _core.FoundMatch(winrt::auto_revoke, { get_weak(), &TermControl::_coreFoundMatch }); + _revokers.UpdateSearchResults = _core.UpdateSearchResults(winrt::auto_revoke, { get_weak(), &TermControl::_coreUpdateSearchResults }); _revokers.UpdateSelectionMarkers = _core.UpdateSelectionMarkers(winrt::auto_revoke, { get_weak(), &TermControl::_updateSelectionMarkers }); _revokers.coreOpenHyperlink = _core.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler }); _revokers.interactivityOpenHyperlink = _interactivity.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler }); @@ -416,6 +416,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // but since code paths differ, extra work is required to ensure correctness. if (!_core.HasMultiLineSelection()) { + _core.SnapSearchResultToSelection(true); const auto selectedLine{ _core.SelectedText(true) }; _searchBox->PopulateTextbox(selectedLine); } @@ -501,7 +502,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/, const RoutedEventArgs& /*args*/) { - _core.ClearSearch(); _searchBox->Close(); // Set focus back to terminal control @@ -3523,17 +3523,41 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // Method Description: - // - Called when the core raises a FoundMatch event. That's done in response - // to us starting a search query with ControlCore::Search. + // - Triggers an update on scrollbar to redraw search scroll marks. + // - Called when the search results have changed, and the scroll marks' + // positions need to be updated. + void TermControl::_UpdateSearchScrollMarks() + { + // Manually send a scrollbar update, on the UI thread. We're already + // UI-driven, so that's okay. We're not really changing the scrollbar, + // but we do want to update the position of any search marks. The Core + // might send a scrollbar update event too, but if the first search hit + // is in the visible viewport, then the pips won't display until the + // user first scrolls. + auto scrollBar = ScrollBar(); + ScrollBarUpdate update{ + .newValue = scrollBar.Value(), + .newMaximum = scrollBar.Maximum(), + .newMinimum = scrollBar.Minimum(), + .newViewportSize = scrollBar.ViewportSize(), + }; + _throttledUpdateScrollbar(update); + } + + // Method Description: + // - Called when the core raises a UpdateSearchResults event. That's done in response to: + // - starting a search query with ControlCore::Search. + // - clearing search results due to change in buffer content. // - The args will tell us if there were or were not any results for that // particular search. We'll use that to control what to announce to // Narrator. When we have more elaborate search information to report, we // may want to report that here. (see GH #3920) // Arguments: - // - args: contains information about the results that were or were not found. + // - args: contains information about the search state and results that were + // or were not found. // Return Value: // - - winrt::fire_and_forget TermControl::_coreFoundMatch(const IInspectable& /*sender*/, Control::FoundResultsArgs args) + winrt::fire_and_forget TermControl::_coreUpdateSearchResults(const IInspectable& /*sender*/, Control::UpdateSearchResultsEventArgs args) { co_await wil::resume_foreground(Dispatcher()); if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) }) @@ -3545,25 +3569,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */); } - // Manually send a scrollbar update, now, on the UI thread. We're - // already UI-driven, so that's okay. We're not really changing the - // scrollbar, but we do want to update the position of any marks. The - // Core might send a scrollbar updated event too, but if the first - // search hit is in the visible viewport, then the pips won't display - // until the user first scrolls. - auto scrollBar = ScrollBar(); - ScrollBarUpdate update{ - .newValue = scrollBar.Value(), - .newMaximum = scrollBar.Maximum(), - .newMinimum = scrollBar.Minimum(), - .newViewportSize = scrollBar.ViewportSize(), - }; - _throttledUpdateScrollbar(update); + _UpdateSearchScrollMarks(); if (_searchBox) { - _searchBox->SetStatus(args.TotalMatches(), args.CurrentMatch()); _searchBox->NavigationEnabled(true); + if (args.State() == Control::SearchState::Inactive) + { + _searchBox->ClearStatus(); + } + else + { + _searchBox->SetStatus(args.TotalMatches(), args.CurrentMatch()); + } } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 24142dc8a7..4240bb23ef 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -352,6 +352,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _SearchChanged(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); + void _UpdateSearchScrollMarks(); // TSFInputControl Handlers void _CompositionCompleted(winrt::hstring text); @@ -365,7 +366,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); - winrt::fire_and_forget _coreFoundMatch(const IInspectable& sender, Control::FoundResultsArgs args); + winrt::fire_and_forget _coreUpdateSearchResults(const IInspectable& sender, Control::UpdateSearchResultsEventArgs args); til::point _toPosInDips(const Core::Point terminalCellPos); void _throttledUpdateScrollbar(const ScrollBarUpdate& update); @@ -394,7 +395,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::ControlCore::TransparencyChanged_revoker TransparencyChanged; Control::ControlCore::RaiseNotice_revoker RaiseNotice; Control::ControlCore::HoveredHyperlinkChanged_revoker HoveredHyperlinkChanged; - Control::ControlCore::FoundMatch_revoker FoundMatch; + Control::ControlCore::UpdateSearchResults_revoker UpdateSearchResults; Control::ControlCore::UpdateSelectionMarkers_revoker UpdateSelectionMarkers; Control::ControlCore::OpenHyperlink_revoker coreOpenHyperlink; Control::ControlCore::TitleChanged_revoker TitleChanged; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 0d2c6dbf43..4eccd3be99 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -1259,6 +1259,35 @@ void Microsoft::Terminal::Core::Terminal::CompletionsChangedCallback(std::functi _pfnCompletionsChanged.swap(pfn); } +void Terminal::SetTextLayoutUpdatedCallback(std::function pfn) noexcept +{ + _pfnTextLayoutUpdated.swap(pfn); +} + +// Method Description: +// - Stores the search highlighted regions in the terminal +void Terminal::SetSearchHighlights(const std::vector& highlights) noexcept +{ + _assertLocked(); + _searchHighlights = highlights; +} + +// Method Description: +// - Stores the focused search highlighted region in the terminal +// - If the region isn't empty, it will be brought into view +void Terminal::SetSearchHighlightFocused(const size_t focusedIdx) +{ + _assertLocked(); + _searchHighlightFocused = focusedIdx; + + // bring the focused region into the view if the index is in valid range + if (focusedIdx < _searchHighlights.size()) + { + const auto focused = til::at(_searchHighlights, focusedIdx); + _ScrollToPoints(focused.start, focused.end); + } +} + Scheme Terminal::GetColorScheme() const { const auto& renderSettings = GetRenderSettings(); diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index fc57907ad1..6a6017db88 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -155,6 +155,7 @@ public: bool IsVtInputEnabled() const noexcept override; void NotifyAccessibilityChange(const til::rect& changedRect) noexcept override; void NotifyBufferRotation(const int delta) override; + void NotifyTextLayoutUpdated() override; void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; @@ -210,12 +211,12 @@ public: std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; std::vector GetSelectionRects() noexcept override; - std::vector GetSearchSelectionRects() noexcept override; + std::span GetSearchHighlights() const noexcept override; + const til::point_span* GetSearchHighlightFocused() const noexcept override; const bool IsSelectionActive() const noexcept override; const bool IsBlockSelection() const noexcept override; void ClearSelection() override; void SelectNewRegion(const til::point coordStart, const til::point coordEnd) override; - void SelectSearchRegions(std::vector source) override; const til::point GetSelectionAnchor() const noexcept override; const til::point GetSelectionEnd() const noexcept override; const std::wstring_view GetConsoleTitle() const noexcept override; @@ -232,6 +233,9 @@ public: void SetShowWindowCallback(std::function pfn) noexcept; void SetPlayMidiNoteCallback(std::function pfn) noexcept; void CompletionsChangedCallback(std::function pfn) noexcept; + void SetTextLayoutUpdatedCallback(std::function pfn) noexcept; + void SetSearchHighlights(const std::vector& highlights) noexcept; + void SetSearchHighlightFocused(const size_t focusedIdx); void BlinkCursor() noexcept; void SetCursorOn(const bool isOn) noexcept; @@ -340,6 +344,7 @@ private: std::function _pfnShowWindowChanged; std::function _pfnPlayMidiNote; std::function _pfnCompletionsChanged; + std::function _pfnTextLayoutUpdated; RenderSettings _renderSettings; std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine; @@ -349,6 +354,9 @@ private: std::wstring _startingTitle; std::optional _startingTabColor; + std::vector _searchHighlights; + size_t _searchHighlightFocused = 0; + CursorType _defaultCursorShape = CursorType::Legacy; til::enumset _systemMode{ Mode::AutoWrap }; @@ -381,7 +389,6 @@ private: til::point pivot; }; std::optional _selection; - std::vector _searchSelections; bool _blockSelection = false; std::wstring _wordDelimiters; SelectionExpansion _multiClickSelectionMode = SelectionExpansion::Char; @@ -447,6 +454,7 @@ private: Microsoft::Console::Types::Viewport _GetVisibleViewport() const noexcept; void _PreserveUserScrollOffset(const int viewportDelta) noexcept; + til::CoordType _ScrollToPoints(const til::point coordStart, const til::point coordEnd); void _NotifyScrollEvent(); @@ -460,7 +468,6 @@ private: #pragma region TextSelection // These methods are defined in TerminalSelection.cpp std::vector _GetSelectionRects() const noexcept; - std::vector _GetSearchSelectionRects(Microsoft::Console::Types::Viewport viewport) const noexcept; std::vector _GetSelectionSpans() const noexcept; std::pair _PivotSelection(const til::point targetPos, bool& targetStart) const noexcept; std::pair _ExpandSelectionAnchors(std::pair anchors) const; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 42f1c903c3..07c81649b7 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -239,6 +239,8 @@ void Terminal::UseAlternateScreenBuffer(const TextAttribute& attrs) // Update scrollbars _NotifyScrollEvent(); + NotifyTextLayoutUpdated(); + // redraw the screen try { @@ -296,6 +298,8 @@ void Terminal::UseMainScreenBuffer() // Update scrollbars _NotifyScrollEvent(); + NotifyTextLayoutUpdated(); + // redraw the screen _activeBuffer().TriggerRedrawAll(); } @@ -370,3 +374,15 @@ void Terminal::NotifyBufferRotation(const int delta) _NotifyScrollEvent(); } } + +// Method Description: +// - Notifies the terminal UI layer that the text layout has changed. +// - This will be called when new text is added, or when the text is +// rearranged in the buffer due to window resize. +void Terminal::NotifyTextLayoutUpdated() +{ + if (_pfnTextLayoutUpdated) + { + _pfnTextLayoutUpdated(); + } +} diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index ae1db3507b..44c9051ed0 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -63,40 +63,6 @@ std::vector Terminal::_GetSelectionRects() const noexcept return result; } -// Method Description: -// - Helper to determine the selected region of the buffer. Used for rendering. -// Return Value: -// - A vector of rectangles representing the regions to select, line by line. They are absolute coordinates relative to the buffer origin. -std::vector Terminal::_GetSearchSelectionRects(Microsoft::Console::Types::Viewport viewport) const noexcept -{ - std::vector result; - try - { - auto lowerIt = std::lower_bound(_searchSelections.begin(), _searchSelections.end(), viewport.Top(), [](const til::inclusive_rect& rect, til::CoordType value) { - return rect.top < value; - }); - - auto upperIt = std::upper_bound(_searchSelections.begin(), _searchSelections.end(), viewport.BottomExclusive(), [](til::CoordType value, const til::inclusive_rect& rect) { - return value < rect.top; - }); - - for (auto selection = lowerIt; selection != upperIt; ++selection) - { - const auto start = til::point{ selection->left, selection->top }; - const auto end = til::point{ selection->right, selection->bottom }; - const auto adj = _activeBuffer().GetTextRects(start, end, _blockSelection, false); - for (auto a : adj) - { - result.emplace_back(a); - } - } - - return result; - } - CATCH_LOG(); - return result; -} - // Method Description: // - Identical to GetTextRects if it's a block selection, else returns a single span for the whole selection. // Return Value: @@ -858,7 +824,6 @@ void Terminal::_MoveByBuffer(SelectionDirection direction, til::point& pos) noex void Terminal::ClearSelection() { _assertLocked(); - _searchSelections.clear(); _selection = std::nullopt; _selectionMode = SelectionInteractionMode::None; _selectionIsTargetingUrl = false; diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 8dd9b4dae9..a9eaf11fa8 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -150,32 +150,37 @@ catch (...) return {}; } -std::vector Terminal::GetSearchSelectionRects() noexcept -try +// Method Description: +// - Helper to determine the search highlights in the buffer. Used for rendering. +// Return Value: +// - A vector of rectangles representing the regions to select, line by line. They are absolute coordinates relative to the buffer origin. +std::span Terminal::GetSearchHighlights() const noexcept { - std::vector result; + _assertLocked(); + return _searchHighlights; +} - for (const auto& lineRect : _GetSearchSelectionRects(_GetVisibleViewport())) +const til::point_span* Terminal::GetSearchHighlightFocused() const noexcept +{ + _assertLocked(); + if (_searchHighlightFocused < _searchHighlights.size()) { - result.emplace_back(Viewport::FromInclusive(lineRect)); + return &til::at(_searchHighlights, _searchHighlightFocused); } - - return result; -} -catch (...) -{ - LOG_CAUGHT_EXCEPTION(); - return {}; + return nullptr; } -void Terminal::SelectNewRegion(const til::point coordStart, const til::point coordEnd) +// Method Description: +// - If necessary, scrolls the viewport such that the start point is in the +// viewport, and if that's already the case, also brings the end point inside +// the viewport +// Arguments: +// - coordStart - The start point +// - coordEnd - The end point +// Return Value: +// - The updated scroll offset +til::CoordType Terminal::_ScrollToPoints(const til::point coordStart, const til::point coordEnd) { -#pragma warning(push) -#pragma warning(disable : 26496) // cpp core checks wants these const, but they're decremented below. - auto realCoordStart = coordStart; - auto realCoordEnd = coordEnd; -#pragma warning(pop) - auto notifyScrollChange = false; if (coordStart.y < _VisibleStartIndex()) { @@ -199,28 +204,18 @@ void Terminal::SelectNewRegion(const til::point coordStart, const til::point coo _NotifyScrollEvent(); } - realCoordStart.y -= _VisibleStartIndex(); - realCoordEnd.y -= _VisibleStartIndex(); - - SetSelectionAnchor(realCoordStart); - SetSelectionEnd(realCoordEnd, SelectionExpansion::Char); + return _scrollOffset; } -void Terminal::SelectSearchRegions(std::vector rects) +void Terminal::SelectNewRegion(const til::point coordStart, const til::point coordEnd) { - _searchSelections.clear(); - for (auto& rect : rects) - { - rect.top -= _VisibleStartIndex(); - rect.bottom -= _VisibleStartIndex(); + const auto newScrollOffset = _ScrollToPoints(coordStart, coordEnd); - const auto realStart = _ConvertToBufferCell(til::point{ rect.left, rect.top }); - const auto realEnd = _ConvertToBufferCell(til::point{ rect.right, rect.bottom }); - - auto rr = til::inclusive_rect{ realStart.x, realStart.y, realEnd.x, realEnd.y }; - - _searchSelections.emplace_back(rr); - } + // update the selection coordinates so they're relative to the new scroll-offset + const auto newCoordStart = til::point{ coordStart.x, coordStart.y - newScrollOffset }; + const auto newCoordEnd = til::point{ coordEnd.x, coordEnd.y - newScrollOffset }; + SetSelectionAnchor(newCoordStart); + SetSelectionEnd(newCoordEnd, SelectionExpansion::Char); } const std::wstring_view Terminal::GetConsoleTitle() const noexcept diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp index 92770d7972..33cf3cee8c 100644 --- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp @@ -51,7 +51,6 @@ namespace HRESULT PaintBufferLine(std::span /*clusters*/, til::point /*coord*/, bool /*fTrimLeft*/, bool /*lineWrapped*/) noexcept { return S_OK; } HRESULT PaintBufferGridLines(GridLineSet /*lines*/, COLORREF /*gridlineColor*/, COLORREF /*underlineColor*/, size_t /*cchLine*/, til::point /*coordTarget*/) noexcept { return S_OK; } HRESULT PaintSelection(const til::rect& /*rect*/) noexcept { return S_OK; } - HRESULT PaintSelections(const std::vector& /*rects*/) noexcept { return S_OK; } HRESULT PaintCursor(const CursorOptions& /*options*/) noexcept { return S_OK; } HRESULT UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, const RenderSettings& /*renderSettings*/, gsl::not_null /*pData*/, bool /*usingSoftFont*/, bool /*isSettingDefaultBrushes*/) noexcept { return S_OK; } HRESULT UpdateFont(const FontInfoDesired& /*FontInfoDesired*/, _Out_ FontInfo& /*FontInfo*/) noexcept { return S_OK; } diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index fb20d74b75..25a2f1fdb2 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -424,7 +424,12 @@ void ConhostInternalGetSet::NotifyBufferRotation(const int delta) } } -void ConhostInternalGetSet::InvokeCompletions(std::wstring_view /*menuJson*/, unsigned int /*replaceLength*/) +void ConhostInternalGetSet::NotifyTextLayoutUpdated() { // Not implemented for conhost. } + +void ConhostInternalGetSet::InvokeCompletions(std::wstring_view /*menuJson*/, unsigned int /*replaceLength*/) +{ + // Not implemented for conhost. +} \ No newline at end of file diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 01d8abaf17..ca5711f638 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -68,6 +68,7 @@ public: void NotifyAccessibilityChange(const til::rect& changedRect) override; void NotifyBufferRotation(const int delta) override; + void NotifyTextLayoutUpdated() override; void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index d9b094626b..c2ad7e99d5 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -81,10 +81,10 @@ std::vector RenderData::GetSelectionRects() noexcept // Method Description: // - Retrieves one rectangle per line describing the area of the viewport -// that should be highlighted in some way to represent a user-interactive selection +// that should be highlighted // Return Value: -// - Vector of Viewports describing the area selected -std::vector RenderData::GetSearchSelectionRects() noexcept +// - Vector of rects describing the highlighted area +std::span RenderData::GetSearchHighlights() const noexcept { return {}; } @@ -381,8 +381,9 @@ void RenderData::SelectNewRegion(const til::point coordStart, const til::point c Selection::Instance().SelectNewRegion(coordStart, coordEnd); } -void RenderData::SelectSearchRegions(std::vector source) +const til::point_span* RenderData::GetSearchHighlightFocused() const noexcept { + return nullptr; } // Routine Description: diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 52056d7a6f..db879fb54e 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -26,7 +26,6 @@ public: const FontInfo& GetFontInfo() const noexcept override; std::vector GetSelectionRects() noexcept override; - std::vector GetSearchSelectionRects() noexcept override; void LockConsole() noexcept override; void UnlockConsole() noexcept override; @@ -55,7 +54,8 @@ public: const bool IsBlockSelection() const noexcept override; void ClearSelection() override; void SelectNewRegion(const til::point coordStart, const til::point coordEnd) override; - void SelectSearchRegions(std::vector source) override; + std::span GetSearchHighlights() const noexcept override; + const til::point_span* GetSearchHighlightFocused() const noexcept override; const til::point GetSelectionAnchor() const noexcept override; const til::point GetSelectionEnd() const noexcept override; const bool IsUiaDataInitialized() const noexcept override { return true; } diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 23d2d3a9c7..dbabcc5a28 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -274,11 +274,6 @@ public: return std::vector{}; } - std::vector GetSearchSelectionRects() noexcept override - { - return std::vector{}; - } - void LockConsole() noexcept override { } @@ -360,8 +355,14 @@ public: { } - void SelectSearchRegions(std::vector /*source*/) override + std::span GetSearchHighlights() const noexcept override { + return {}; + } + + const til::point_span* GetSearchHighlightFocused() const noexcept override + { + return nullptr; } const til::point GetSelectionAnchor() const noexcept diff --git a/src/inc/til/point.h b/src/inc/til/point.h index 678afe706f..17bc9d1c86 100644 --- a/src/inc/til/point.h +++ b/src/inc/til/point.h @@ -288,6 +288,40 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { return __builtin_memcmp(this, &rhs, sizeof(rhs)) != 0; } + + // Calls func(row, begX, endX) for each row and begX and begY are inclusive coordinates, + // because point_span itself also uses inclusive coordinates. + // In other words, it turns a + // ``` + // +----------------+ + // | #########| + // |################| + // |#### | + // +----------------+ + // ``` + // into: + // ``` + // func(0, 8, 15) + // func(1, 0, 15) + // func(2, 0, 4) + // ``` + constexpr void iterate_rows(til::CoordType width, auto&& func) const + { + // Copy the members so that the compiler knows it doesn't + // need to re-read them on every loop iteration. + const auto w = width - 1; + const auto ax = std::clamp(start.x, 0, w); + const auto ay = start.y; + const auto bx = std::clamp(end.x, 0, w); + const auto by = end.y; + + for (auto y = ay; y <= by; ++y) + { + const auto x1 = y != ay ? 0 : ax; + const auto x2 = y != by ? w : bx; + func(y, x1, x2); + } + } }; } diff --git a/src/inc/til/rect.h b/src/inc/til/rect.h index bc7b07b6c8..a62500950e 100644 --- a/src/inc/til/rect.h +++ b/src/inc/til/rect.h @@ -201,6 +201,20 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return __builtin_memcmp(this, &rhs, sizeof(rhs)) != 0; } + constexpr rect to_origin(const rect& other) const + { + return to_origin(other.origin()); + } + + constexpr rect to_origin(const point& origin) const + { + const auto l = details::extract(::base::CheckSub(left, origin.x)); + const auto t = details::extract(::base::CheckSub(top, origin.y)); + const auto r = details::extract(::base::CheckSub(right, origin.x)); + const auto b = details::extract(::base::CheckSub(bottom, origin.y)); + return { l, t, r, b }; + } + explicit constexpr operator bool() const noexcept { return left >= 0 && top >= 0 && right > left && bottom > top; diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index a730ae8219..d508603d5f 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -161,11 +161,6 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT BgfxEngine::PaintSelections(const std::vector& /*rects*/) noexcept -{ - return S_OK; -} - [[nodiscard]] HRESULT BgfxEngine::PaintCursor(const CursorOptions& options) noexcept try { diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index c787baba39..e9759ce030 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -53,7 +53,6 @@ namespace Microsoft::Console::Render const bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; - [[nodiscard]] HRESULT PaintSelections(const std::vector& rects) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 9c98a12cf5..9ef05279bf 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -95,6 +95,28 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2& out) noexcept return S_OK; } +[[nodiscard]] HRESULT AtlasEngine::InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept +{ + const auto viewportOrigin = til::point{ _api.s->viewportOffset.x, _api.s->viewportOffset.y }; + const auto viewport = til::rect{ 0, 0, _api.s->viewportCellCount.x, _api.s->viewportCellCount.y }; + const auto cellCountX = static_cast(_api.s->viewportCellCount.x); + for (const auto& hi : highlights) + { + hi.iterate_rows(cellCountX, [&](til::CoordType row, til::CoordType beg, til::CoordType end) { + const auto shift = til::at(renditions, row) != LineRendition::SingleWidth ? 1 : 0; + beg <<= shift; + end <<= shift; + til::rect rect{ beg, row, end + 1, row + 1 }; + rect = rect.to_origin(viewportOrigin); + rect &= viewport; + _api.invalidatedRows.start = gsl::narrow_cast(std::min(_api.invalidatedRows.start, std::max(0, rect.top))); + _api.invalidatedRows.end = gsl::narrow_cast(std::max(_api.invalidatedRows.end, std::max(0, rect.bottom))); + }); + } + + return S_OK; +} + [[nodiscard]] HRESULT AtlasEngine::InvalidateScroll(const til::point* const pcoordDelta) noexcept { // InvalidateScroll() is a "synchronous" API. Any Invalidate()s after diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 8cfbe995a1..f1e43bc98a 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -285,8 +285,36 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT AtlasEngine::PrepareRenderInfo(const RenderFrameInfo& info) noexcept +[[nodiscard]] HRESULT AtlasEngine::PrepareRenderInfo(RenderFrameInfo info) noexcept { + // remove the highlighted regions that falls outside of the dirty region + { + const auto& highlights = info.searchHighlights; + + // get the buffer origin relative to the viewport, and use it to calculate + // the dirty region to be relative to the buffer origin + const til::CoordType offsetX = _p.s->viewportOffset.x; + const til::CoordType offsetY = _p.s->viewportOffset.y; + const til::point bufferOrigin{ -offsetX, -offsetY }; + const auto dr = _api.dirtyRect.to_origin(bufferOrigin); + + const auto hiBeg = std::lower_bound(highlights.begin(), highlights.end(), dr.top, [](const auto& ps, const auto& drTop) { return ps.end.y < drTop; }); + const auto hiEnd = std::upper_bound(hiBeg, highlights.end(), dr.bottom, [](const auto& drBottom, const auto& ps) { return drBottom < ps.start.y; }); + _api.searchHighlights = { hiBeg, hiEnd }; + + // do the same for the focused search highlight + if (info.searchHighlightFocused) + { + const auto focusedStartY = info.searchHighlightFocused->start.y; + const auto focusedEndY = info.searchHighlightFocused->end.y; + const auto isFocusedInside = (focusedStartY >= dr.top && focusedStartY < dr.bottom) || (focusedEndY >= dr.top && focusedEndY < dr.bottom) || (focusedStartY < dr.top && focusedEndY >= dr.bottom); + if (isFocusedInside) + { + _api.searchHighlightFocused = { info.searchHighlightFocused, 1 }; + } + } + } + return S_OK; } @@ -308,6 +336,126 @@ CATCH_RETURN() return S_OK; } +void AtlasEngine::_fillColorBitmap(const size_t y, const size_t x1, const size_t x2, const u32 fgColor, const u32 bgColor) noexcept +{ + const auto bitmap = _p.colorBitmap.begin() + _p.colorBitmapRowStride * y; + const auto shift = gsl::narrow_cast(_p.rows[y]->lineRendition != LineRendition::SingleWidth); + auto beg = bitmap + (x1 << shift); + auto end = bitmap + (x2 << shift); + + const u32 colors[] = { + u32ColorPremultiply(bgColor), + fgColor, + }; + + // This fills the color in the background bitmap, and then in the foreground bitmap. + for (size_t i = 0; i < 2; ++i) + { + const auto color = colors[i]; + + for (auto it = beg; it != end; ++it) + { + if (*it != color) + { + _p.colorBitmapGenerations[i].bump(); + std::fill(it, end, color); + break; + } + } + + // go to the same range in the same row, but in the foreground bitmap + beg += _p.colorBitmapDepthStride; + end += _p.colorBitmapDepthStride; + } +} + +// Method Description: +// - Applies the given highlighting colors to the columns in the highlighted regions within a given range. +// - Resumes from the last partially painted region if any. +// Arguments: +// - highlights: the list of highlighted regions (yet to be painted) +// - row: the row for which highlighted regions are to be painted +// - begX: the starting (inclusive) column to paint from (in Buffer coord) +// - endX: the ending (exclusive) column to paint up to (in Buffer coord) +// - fgColor: the foreground highlight color +// - bgColor: the background highlight color +// Returns: +// - S_OK if we painted successfully, else an appropriate HRESULT error code +[[nodiscard]] HRESULT AtlasEngine::_drawHighlighted(std::span& highlights, const u16 row, const u16 begX, const u16 endX, const u32 fgColor, const u32 bgColor) noexcept +try +{ + if (highlights.empty()) + { + return S_OK; + } + + const til::CoordType y = row; + const til::CoordType x1 = begX; + const til::CoordType x2 = endX; + const auto offset = til::point{ _p.s->viewportOffset.x, _p.s->viewportOffset.y }; + auto it = highlights.begin(); + const auto itEnd = highlights.end(); + auto hiStart = it->start - offset; + auto hiEnd = it->end - offset; + + // Nothing to paint if we haven't reached the row where the highlight region begins + if (y < hiStart.y) + { + return S_OK; + } + + // We might have painted a multi-line highlight region in the last call, + // so we need to make sure we paint the whole region before moving on to + // the next region. + if (y > hiStart.y) + { + const auto isFinalRow = y == hiEnd.y; + const auto end = isFinalRow ? std::min(hiEnd.x + 1, x2) : x2; + _fillColorBitmap(row, x1, end, fgColor, bgColor); + + // Return early if we couldn't paint the whole region. We will resume + // from here in the next call. + if (!isFinalRow || end == x2) + { + return S_OK; + } + + ++it; + } + + // Pick a highlighted region and (try) to paint it + while (it != itEnd) + { + hiStart = it->start - offset; + hiEnd = it->end - offset; + + const auto isStartInside = y == hiStart.y && hiStart.x < x2; + const auto isEndInside = y == hiEnd.y && hiEnd.x < x2; + if (isStartInside && isEndInside) + { + _fillColorBitmap(row, hiStart.x, static_cast(hiEnd.x) + 1, fgColor, bgColor); + ++it; + } + else + { + // Paint the region partially if start is within the range. + if (isStartInside) + { + const auto start = std::max(x1, hiStart.x); + _fillColorBitmap(y, start, x2, fgColor, bgColor); + } + + break; + } + } + + // Shorten the list by removing processed regions + highlights = { it, itEnd }; + + return S_OK; +} +CATCH_RETURN() + [[nodiscard]] HRESULT AtlasEngine::PaintBufferLine(std::span clusters, til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept try { @@ -345,34 +493,12 @@ try _api.bufferLineColumn.emplace_back(columnEnd); } - { - const auto row = _p.colorBitmap.begin() + _p.colorBitmapRowStride * y; - auto beg = row + (static_cast(x) << shift); - auto end = row + (static_cast(columnEnd) << shift); + // Apply the current foreground and background colors to the cells + _fillColorBitmap(y, x, columnEnd, _api.currentForeground, _api.currentBackground); - const u32 colors[] = { - u32ColorPremultiply(_api.currentBackground), - _api.currentForeground, - }; - - for (size_t i = 0; i < 2; ++i) - { - const auto color = colors[i]; - - for (auto it = beg; it != end; ++it) - { - if (*it != color) - { - _p.colorBitmapGenerations[i].bump(); - std::fill(it, end, color); - break; - } - } - - beg += _p.colorBitmapDepthStride; - end += _p.colorBitmapDepthStride; - } - } + // Apply the highlighting colors to the highlighted cells + RETURN_IF_FAILED(_drawHighlighted(_api.searchHighlights, y, x, columnEnd, highlightFg, highlightBg)); + RETURN_IF_FAILED(_drawHighlighted(_api.searchHighlightFocused, y, x, columnEnd, highlightFocusFg, highlightFocusBg)); _api.lastPaintBufferLineCoord = { x, y }; return S_OK; @@ -418,40 +544,6 @@ try } CATCH_RETURN() -[[nodiscard]] HRESULT AtlasEngine::PaintSelections(const std::vector& rects) noexcept -try -{ - if (rects.empty()) - { - return S_OK; - } - - for (const auto& rect : rects) - { - const auto y = gsl::narrow_cast(clamp(rect.top, 0, _p.s->viewportCellCount.y)); - const auto from = gsl::narrow_cast(clamp(rect.left, 0, _p.s->viewportCellCount.x - 1)); - const auto to = gsl::narrow_cast(clamp(rect.right, from, _p.s->viewportCellCount.x)); - - if (rect.bottom <= 0 || rect.top >= _p.s->viewportCellCount.y) - { - continue; - } - - const auto bg = &_p.backgroundBitmap[_p.colorBitmapRowStride * y]; - const auto fg = &_p.foregroundBitmap[_p.colorBitmapRowStride * y]; - std::fill(bg + from, bg + to, 0xff3296ff); - std::fill(fg + from, fg + to, 0xff000000); - } - - for (int i = 0; i < 2; ++i) - { - _p.colorBitmapGenerations[i].bump(); - } - - return S_OK; -} -CATCH_RETURN() - [[nodiscard]] HRESULT AtlasEngine::PaintCursor(const CursorOptions& options) noexcept try { diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 6d346d7bfe..9f38b7094f 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -33,19 +33,19 @@ namespace Microsoft::Console::Render::Atlas [[nodiscard]] HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept override; [[nodiscard]] HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; + [[nodiscard]] HRESULT InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept override; [[nodiscard]] HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept override; [[nodiscard]] HRESULT InvalidateAll() noexcept override; [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override; [[nodiscard]] HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept override; [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override; - [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; + [[nodiscard]] HRESULT PrepareRenderInfo(RenderFrameInfo info) noexcept override; [[nodiscard]] HRESULT ResetLineTransform() noexcept override; [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; - [[nodiscard]] HRESULT PaintSelections(const std::vector& rects) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; @@ -91,6 +91,8 @@ namespace Microsoft::Console::Render::Atlas void _mapCharacters(const wchar_t* text, u32 textLength, u32* mappedLength, IDWriteFontFace2** mappedFontFace) const; void _mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 length, ShapedRow& row); ATLAS_ATTR_COLD void _mapReplacementCharacter(u32 from, u32 to, ShapedRow& row); + void _fillColorBitmap(const size_t y, const size_t x1, const size_t x2, const u32 fgColor, const u32 bgColor) noexcept; + [[nodiscard]] HRESULT _drawHighlighted(std::span& highlights, const u16 row, const u16 begX, const u16 endX, const u32 fgColor, const u32 bgColor) noexcept; // AtlasEngine.api.cpp void _resolveTransparencySettings() noexcept; @@ -117,6 +119,11 @@ namespace Microsoft::Console::Render::Atlas static constexpr range invalidatedRowsNone{ u16max, u16min }; static constexpr range invalidatedRowsAll{ u16min, u16max }; + static constexpr u32 highlightBg = 0xff00ffff; + static constexpr u32 highlightFg = 0xff000000; + static constexpr u32 highlightFocusBg = 0xff3296ff; + static constexpr u32 highlightFocusFg = 0xff000000; + std::unique_ptr _b; RenderingPayload _p; @@ -173,6 +180,10 @@ namespace Microsoft::Console::Render::Atlas // UpdateHyperlinkHoveredId() u16 hyperlinkHoveredId = 0; + // These tracks the highlighted regions on the screen that are yet to be painted. + std::span searchHighlights; + std::span searchHighlightFocused; + // dirtyRect is a computed value based on invalidatedRows. til::rect dirtyRect; // These "invalidation" fields are reset in EndPaint() diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index 224ff155f3..6ab523a1a4 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -7,6 +7,11 @@ using namespace Microsoft::Console; using namespace Microsoft::Console::Render; +[[nodiscard]] HRESULT RenderEngineBase::InvalidateHighlight(std::span /*highlights*/, const std::vector& /*renditions*/) noexcept +{ + return S_OK; +} + HRESULT RenderEngineBase::InvalidateTitle(const std::wstring_view proposedTitle) noexcept { if (proposedTitle != _lastFrameTitle) @@ -42,7 +47,7 @@ HRESULT RenderEngineBase::UpdateSoftFont(const std::span /*bitPa return S_FALSE; } -HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noexcept +HRESULT RenderEngineBase::PrepareRenderInfo(RenderFrameInfo /*info*/) noexcept { return S_FALSE; } diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 4137d140d3..2d6f4cc1a2 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -360,7 +360,6 @@ void Renderer::TriggerSelection() { // Get selection rectangles auto rects = _GetSelectionRects(); - auto searchSelections = _GetSearchSelectionRects(); // Make a viewport representing the coordinates that are currently presentable. const til::rect viewport{ _pData->GetViewport().Dimensions() }; @@ -373,20 +372,45 @@ void Renderer::TriggerSelection() FOREACH_ENGINE(pEngine) { - LOG_IF_FAILED(pEngine->InvalidateSelection(_previousSearchSelection)); LOG_IF_FAILED(pEngine->InvalidateSelection(_previousSelection)); - LOG_IF_FAILED(pEngine->InvalidateSelection(searchSelections)); LOG_IF_FAILED(pEngine->InvalidateSelection(rects)); } _previousSelection = std::move(rects); - _previousSearchSelection = std::move(searchSelections); - NotifyPaintFrame(); } CATCH_LOG(); } +// Routine Description: +// - Called when the search highlight areas in the console have changed. +void Renderer::TriggerSearchHighlight(const std::vector& oldHighlights) +try +{ + const auto& buffer = _pData->GetTextBuffer(); + const auto rows = buffer.TotalRowCount(); + + std::vector renditions; + renditions.reserve(rows); + for (til::CoordType row = 0; row < rows; ++row) + { + renditions.emplace_back(buffer.GetLineRendition(row)); + } + + // no need to invalidate focused search highlight separately as they are + // included in (all) search highlights. + const auto newHighlights = _pData->GetSearchHighlights(); + + FOREACH_ENGINE(pEngine) + { + LOG_IF_FAILED(pEngine->InvalidateHighlight(oldHighlights, renditions)); + LOG_IF_FAILED(pEngine->InvalidateHighlight(newHighlights, renditions)); + } + + NotifyPaintFrame(); +} +CATCH_LOG() + // Routine Description: // - Called when we want to check if the viewport has moved and scroll accordingly if so. // Arguments: @@ -1129,8 +1153,9 @@ void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine) [[nodiscard]] HRESULT Renderer::_PrepareRenderInfo(_In_ IRenderEngine* const pEngine) { RenderFrameInfo info; - info.cursorInfo = _currentCursorOptions; - return pEngine->PrepareRenderInfo(info); + info.searchHighlights = _pData->GetSearchHighlights(); + info.searchHighlightFocused = _pData->GetSearchHighlightFocused(); + return pEngine->PrepareRenderInfo(std::move(info)); } // Routine Description: @@ -1212,19 +1237,10 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) // Get selection rectangles const auto rectangles = _GetSelectionRects(); - const auto searchRectangles = _GetSearchSelectionRects(); std::vector dirtySearchRectangles; for (auto& dirtyRect : dirtyAreas) { - for (const auto& sr : searchRectangles) - { - if (const auto rectCopy = sr & dirtyRect) - { - dirtySearchRectangles.emplace_back(rectCopy); - } - } - for (const auto& rect : rectangles) { if (const auto rectCopy = rect & dirtyRect) @@ -1233,11 +1249,6 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) } } } - - if (!dirtySearchRectangles.empty()) - { - LOG_IF_FAILED(pEngine->PaintSelections(std::move(dirtySearchRectangles))); - } } CATCH_LOG(); } @@ -1303,28 +1314,6 @@ std::vector Renderer::_GetSelectionRects() const return result; } -std::vector Renderer::_GetSearchSelectionRects() const -{ - const auto& buffer = _pData->GetTextBuffer(); - auto rects = _pData->GetSearchSelectionRects(); - // Adjust rectangles to viewport - auto view = _pData->GetViewport(); - - std::vector result; - result.reserve(rects.size()); - - for (auto rect : rects) - { - // Convert buffer offsets to the equivalent range of screen cells - // expected by callers, taking line rendition into account. - const auto lineRendition = buffer.GetLineRendition(rect.Top()); - rect = Viewport::FromInclusive(BufferToScreenLine(rect.ToInclusive(), lineRendition)); - result.emplace_back(view.ConvertToOrigin(rect).ToExclusive()); - } - - return result; -} - // Method Description: // - Offsets all of the selection rectangles we might be holding onto // as the previously selected area. If the whole viewport scrolls, diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index dc027acea2..a8184ce63d 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -54,6 +54,7 @@ namespace Microsoft::Console::Render void TriggerTeardown() noexcept; void TriggerSelection(); + void TriggerSearchHighlight(const std::vector& oldHighlights); void TriggerScroll(); void TriggerScroll(const til::point* const pcoordDelta); @@ -110,7 +111,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute attr, const bool usingSoftFont, const bool isSettingDefaultBrushes); [[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); std::vector _GetSelectionRects() const; - std::vector _GetSearchSelectionRects() const; void _ScrollPreviousSelection(const til::point delta); [[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine); bool _isInHoveredInterval(til::point coordTarget) const noexcept; @@ -129,7 +129,6 @@ namespace Microsoft::Console::Render std::optional _currentCursorOptions; std::vector _clusterBuffer; std::vector _previousSelection; - std::vector _previousSearchSelection; std::function _pfnBackgroundColorChanged; std::function _pfnFrameColorChanged; std::function _pfnRendererEnteredErrorState; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 601085c35f..78a3f02953 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -57,7 +57,6 @@ namespace Microsoft::Console::Render const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; - [[nodiscard]] HRESULT PaintSelections(const std::vector& rects) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index c449b4d083..6fd8d7521e 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -834,13 +834,6 @@ CATCH_RETURN(); return S_OK; } -[[nodiscard]] HRESULT GdiEngine::PaintSelections(const std::vector& rects) noexcept -{ - UNREFERENCED_PARAMETER(rects); - - return S_OK; -} - #ifdef DBG void GdiEngine::_CreateDebugWindow() diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index cfc035a7f9..f9c08c83b9 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -47,7 +47,8 @@ namespace Microsoft::Console::Render virtual const TextBuffer& GetTextBuffer() const noexcept = 0; virtual const FontInfo& GetFontInfo() const noexcept = 0; virtual std::vector GetSelectionRects() noexcept = 0; - virtual std::vector GetSearchSelectionRects() noexcept = 0; + virtual std::span GetSearchHighlights() const noexcept = 0; + virtual const til::point_span* GetSearchHighlightFocused() const noexcept = 0; virtual void LockConsole() noexcept = 0; virtual void UnlockConsole() noexcept = 0; @@ -72,7 +73,6 @@ namespace Microsoft::Console::Render virtual const bool IsBlockSelection() const = 0; virtual void ClearSelection() = 0; virtual void SelectNewRegion(const til::point coordStart, const til::point coordEnd) = 0; - virtual void SelectSearchRegions(std::vector source) = 0; virtual const til::point GetSelectionAnchor() const noexcept = 0; virtual const til::point GetSelectionEnd() const noexcept = 0; virtual const bool IsUiaDataInitialized() const noexcept = 0; diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 5dcaf17685..7533de8a4d 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -29,7 +29,8 @@ namespace Microsoft::Console::Render { struct RenderFrameInfo { - std::optional cursorInfo; + std::span searchHighlights; + const til::point_span* searchHighlightFocused; }; enum class GridLines @@ -66,19 +67,19 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateSelection(const std::vector& rectangles) noexcept = 0; + [[nodiscard]] virtual HRESULT InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateAll() noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept = 0; [[nodiscard]] virtual HRESULT NotifyNewText(const std::wstring_view newText) noexcept = 0; - [[nodiscard]] virtual HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept = 0; + [[nodiscard]] virtual HRESULT PrepareRenderInfo(RenderFrameInfo info) noexcept = 0; [[nodiscard]] virtual HRESULT ResetLineTransform() noexcept = 0; [[nodiscard]] virtual HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept = 0; [[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0; [[nodiscard]] virtual HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept = 0; [[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF gridlineColor, COLORREF underlineColor, size_t cchLine, til::point coordTarget) noexcept = 0; [[nodiscard]] virtual HRESULT PaintSelection(const til::rect& rect) noexcept = 0; - [[nodiscard]] virtual HRESULT PaintSelections(const std::vector& rects) noexcept = 0; [[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept = 0; diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 6390f5fe7f..5fd8a94c8a 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -24,6 +24,7 @@ namespace Microsoft::Console::Render class RenderEngineBase : public IRenderEngine { public: + [[nodiscard]] HRESULT InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept override; [[nodiscard]] HRESULT InvalidateTitle(const std::wstring_view proposedTitle) noexcept override; [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override; @@ -34,7 +35,7 @@ namespace Microsoft::Console::Render const til::size cellSize, const size_t centeringHint) noexcept override; - [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; + [[nodiscard]] HRESULT PrepareRenderInfo(RenderFrameInfo info) noexcept override; [[nodiscard]] HRESULT ResetLineTransform() noexcept override; [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index a245491dea..d1e72fbd0e 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -386,11 +386,6 @@ void UiaEngine::WaitUntilCanRender() noexcept return S_FALSE; } -[[nodiscard]] HRESULT UiaEngine::PaintSelections(const std::vector& /*rect*/) noexcept -{ - return S_FALSE; -} - // Routine Description: // - Draws the cursor on the screen // For UIA, this doesn't mean anything. So do nothing. diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 5d244dcc13..3fd069f425 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -51,7 +51,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; - [[nodiscard]] HRESULT PaintSelections(const std::vector& rects) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, const gsl::not_null pData, const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index 6dc611252c..af08cdd5b5 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -245,11 +245,6 @@ using namespace Microsoft::Console::Types; return S_OK; } -[[nodiscard]] HRESULT VtEngine::PaintSelections(const std::vector& /*rect*/) noexcept -{ - return S_OK; -} - // Routine Description: // - Write a VT sequence to change the current colors of text. Writes true RGB // color sequences. diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 3bea4528ac..0135e79116 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -64,7 +64,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; - [[nodiscard]] HRESULT PaintSelections(const std::vector& rects) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int iDpi) noexcept override; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index db729467ca..fda8ae6c21 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -298,11 +298,6 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT WddmConEngine::PaintSelections(const std::vector& /*rects*/) noexcept -{ - return S_OK; -} - [[nodiscard]] HRESULT WddmConEngine::PaintCursor(const CursorOptions& /*options*/) noexcept { return S_OK; diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index f37146daf5..1110c87078 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -46,7 +46,6 @@ namespace Microsoft::Console::Render const bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; - [[nodiscard]] HRESULT PaintSelections(const std::vector& rects) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index 4381c4ecbd..b42ffe283f 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -80,6 +80,7 @@ namespace Microsoft::Console::VirtualTerminal virtual void NotifyAccessibilityChange(const til::rect& changedRect) = 0; virtual void NotifyBufferRotation(const int delta) = 0; + virtual void NotifyTextLayoutUpdated() = 0; virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0; }; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 6f0681ad58..1a887f99f8 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -178,9 +178,11 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) _ApplyCursorMovementFlags(cursor); - // Notify UIA of new text. + // Notify terminal and UIA of new text. // It's important to do this here instead of in TextBuffer, because here you - // have access to the entire line of text. + // have access to the entire line of text, whereas TextBuffer writes it one + // character at a time via the OutputCellIterator. + _api.NotifyTextLayoutUpdated(); textBuffer.TriggerNewTextNotification(string); } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 5cd5fbdc29..b17bc4429f 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -215,6 +215,11 @@ public: Log::Comment(L"NotifyBufferRotation MOCK called..."); } + void NotifyTextLayoutUpdated() override + { + Log::Comment(L"NotifyTextLayoutUpdated MOCK called..."); + } + void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override { Log::Comment(L"InvokeCompletions MOCK called..."); From 643f7167a66428c777f1e59b03a38279a5c2b688 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 17 Apr 2024 10:52:29 -0700 Subject: [PATCH 05/53] Also remember to persist window positions (#17066) This got lost in #16598. `TerminalPage` needs to ask the window where the it actually is, so it can persist it. More details in https://github.com/microsoft/terminal/pull/16598#discussion_r1511519304 Closes #17010 --- src/cascadia/TerminalApp/TerminalPage.cpp | 7 +++++++ src/cascadia/TerminalApp/TerminalPage.h | 10 ++++++++++ src/cascadia/TerminalApp/TerminalPage.idl | 7 +++++++ src/cascadia/TerminalApp/TerminalWindow.h | 2 ++ src/cascadia/TerminalApp/TerminalWindow.idl | 1 + src/cascadia/WindowsTerminal/AppHost.cpp | 9 +++++++++ src/cascadia/WindowsTerminal/AppHost.h | 4 ++++ 7 files changed, 40 insertions(+) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index ff4b6743e6..1749acce26 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -8,6 +8,7 @@ #include "RenameWindowRequestedArgs.g.cpp" #include "RequestMoveContentArgs.g.cpp" #include "RequestReceiveContentArgs.g.cpp" +#include "LaunchPositionRequest.g.cpp" #include @@ -1952,6 +1953,12 @@ namespace winrt::TerminalApp::implementation layout.InitialSize(windowSize); + // We don't actually know our own position. So we have to ask the window + // layer for that. + const auto launchPosRequest{ winrt::make() }; + RequestLaunchPosition.raise(*this, launchPosRequest); + layout.InitialPosition(launchPosRequest.Position()); + ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout); } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 711d9f7b7e..0dba78abd4 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -10,6 +10,7 @@ #include "RenameWindowRequestedArgs.g.h" #include "RequestMoveContentArgs.g.h" #include "RequestReceiveContentArgs.g.h" +#include "LaunchPositionRequest.g.h" #include "Toast.h" #define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); @@ -79,6 +80,13 @@ namespace winrt::TerminalApp::implementation _TabIndex{ tabIndex } {}; }; + struct LaunchPositionRequest : LaunchPositionRequestT + { + LaunchPositionRequest() = default; + + til::property Position; + }; + struct TerminalPage : TerminalPageT { public: @@ -186,6 +194,8 @@ namespace winrt::TerminalApp::implementation til::typed_event RequestMoveContent; til::typed_event RequestReceiveContent; + til::typed_event RequestLaunchPosition; + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 8ec4fa1dc9..667350056b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -51,6 +51,11 @@ namespace TerminalApp Boolean IsQuakeWindow(); }; + runtimeclass LaunchPositionRequest + { + Microsoft.Terminal.Settings.Model.LaunchPosition Position; + } + [default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, Microsoft.Terminal.UI.IDirectKeyListener { TerminalPage(WindowProperties properties, ContentManager manager); @@ -98,5 +103,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestMoveContent; event Windows.Foundation.TypedEventHandler RequestReceiveContent; + + event Windows.Foundation.TypedEventHandler RequestLaunchPosition; } } diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index d48d8d4f24..b10c29a8d7 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -231,6 +231,8 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(RequestMoveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestMoveContentArgs, _root, RequestMoveContent); FORWARDED_TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs, _root, RequestReceiveContent); + FORWARDED_TYPED_EVENT(RequestLaunchPosition, Windows::Foundation::IInspectable, winrt::TerminalApp::LaunchPositionRequest, _root, RequestLaunchPosition); + #ifdef UNIT_TESTING friend class TerminalAppLocalTests::CommandlineTest; #endif diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index 1700686e1d..75c47fce8b 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -137,6 +137,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestMoveContent; event Windows.Foundation.TypedEventHandler RequestReceiveContent; + event Windows.Foundation.TypedEventHandler RequestLaunchPosition; void AttachContent(String content, UInt32 tabIndex); void SendContentToOther(RequestReceiveContentArgs args); diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 0fe39c1cc5..5470db596f 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -341,6 +341,7 @@ void AppHost::Initialize() _revokers.RaiseVisualBell = _windowLogic.RaiseVisualBell(winrt::auto_revoke, { this, &AppHost::_RaiseVisualBell }); _revokers.SystemMenuChangeRequested = _windowLogic.SystemMenuChangeRequested(winrt::auto_revoke, { this, &AppHost::_SystemMenuChangeRequested }); _revokers.ChangeMaximizeRequested = _windowLogic.ChangeMaximizeRequested(winrt::auto_revoke, { this, &AppHost::_ChangeMaximizeRequested }); + _revokers.RequestLaunchPosition = _windowLogic.RequestLaunchPosition(winrt::auto_revoke, { this, &AppHost::_HandleRequestLaunchPosition }); _windowCallbacks.MaximizeChanged = _window->MaximizeChanged([this](bool newMaximize) { if (_windowLogic) @@ -510,6 +511,14 @@ void AppHost::AppTitleChanged(const winrt::Windows::Foundation::IInspectable& /* _windowManager.UpdateActiveTabTitle(newTitle, _peasant); } +// The terminal page is responsible for persisting it's own state, but it does +// need to ask us where exactly on the screen the window is. +void AppHost::_HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& /*sender*/, + winrt::TerminalApp::LaunchPositionRequest args) +{ + args.Position(_GetWindowLaunchPosition()); +} + LaunchPosition AppHost::_GetWindowLaunchPosition() { LaunchPosition pos{}; diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index a22fc85a94..cc90c21c1c 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -160,6 +160,9 @@ private: void _stopFrameTimer(); void _updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); + void _HandleRequestLaunchPosition(const winrt::Windows::Foundation::IInspectable& sender, + winrt::TerminalApp::LaunchPositionRequest args); + // Helper struct. By putting these all into one struct, we can revoke them // all at once, by assigning _revokers to a fresh Revokers instance. That'll // cause us to dtor the old one, which will immediately call revoke on all @@ -195,6 +198,7 @@ private: winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged; winrt::TerminalApp::TerminalWindow::RequestMoveContent_revoker RequestMoveContent; winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent; + winrt::TerminalApp::TerminalWindow::RequestLaunchPosition_revoker RequestLaunchPosition; winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged; winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged; From 06ab6f3e1f2143a1b2681dd954df2a67041373eb Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Wed, 17 Apr 2024 17:30:25 -0700 Subject: [PATCH 06/53] Add IDs to Commands (#16904) As laid out in #16816, adds an `ID` field to `Command`. **This first PR only adds IDs for built-in commands in defaults, and generates IDs for user-created commands that don't define an ID.** Also note that for now we **will not** be allowing IDs for iterable/nested commands. The follow-up PR is where we will actually use the IDs by referring to commands with them. Refs #16816 ## Validation Steps Performed User-created commands in the settings file get rewritten with generated IDs --- .../TerminalSettingsModel/ActionAndArgs.cpp | 30 +++ .../TerminalSettingsModel/ActionAndArgs.h | 1 + .../TerminalSettingsModel/ActionMap.cpp | 19 ++ .../TerminalSettingsModel/ActionMap.h | 1 + .../CascadiaSettingsSerialization.cpp | 4 + .../TerminalSettingsModel/Command.cpp | 32 ++- src/cascadia/TerminalSettingsModel/Command.h | 4 + .../TerminalSettingsModel/Command.idl | 1 + .../TerminalSettingsModel/defaults.json | 250 +++++++++--------- .../SerializationTests.cpp | 137 +++++++++- 10 files changed, 352 insertions(+), 127 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 66126cda15..7bba13bc4c 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -450,6 +450,36 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return found != GeneratedActionNames.end() ? found->second : L""; } + // Function Description: + // - This will generate an ID for this ActionAndArgs, based on the ShortcutAction and the Args + // - It will always create the same ID if the ShortcutAction and the Args are the same + // - Note: this should only be called for User-created actions + // - Example: The "SendInput 'abc'" action will have the generated ID "User.sendInput." + // Return Value: + // - The ID, based on the ShortcutAction and the Args + winrt::hstring ActionAndArgs::GenerateID() const + { + if (_Action != ShortcutAction::Invalid) + { + auto actionKeyString = ActionToStringMap.find(_Action)->second; + auto result = fmt::format(FMT_COMPILE(L"User.{}"), actionKeyString); + if (_Args) + { + // If there are args, we need to append the hash of the args + // However, to make it a little more presentable we + // 1. truncate the hash to 32 bits + // 2. convert it to a hex string + // there is a _tiny_ chance of collision because of the truncate but unlikely for + // the number of commands a user is expected to have + const auto argsHash32 = static_cast(_Args.Hash() & 0xFFFFFFFF); + // {0:X} formats the truncated hash to an uppercase hex string + fmt::format_to(std::back_inserter(result), FMT_COMPILE(L".{:X}"), argsHash32); + } + return winrt::hstring{ result }; + } + return L""; + } + winrt::hstring ActionAndArgs::Serialize(const winrt::Windows::Foundation::Collections::IVector& args) { Json::Value json{ Json::objectValue }; diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.h b/src/cascadia/TerminalSettingsModel/ActionAndArgs.h index a2afefaa49..9c841ca727 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.h @@ -27,6 +27,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation com_ptr Copy() const; hstring GenerateName() const; + hstring GenerateID() const; WINRT_PROPERTY(ShortcutAction, Action, ShortcutAction::Invalid); WINRT_PROPERTY(IActionArgs, Args, nullptr); diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 76565dff44..220cd82556 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -795,6 +795,25 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return nullptr; } + bool ActionMap::GenerateIDsForActions() + { + bool fixedUp{ false }; + for (auto actionPair : _ActionMap) + { + auto cmdImpl{ winrt::get_self(actionPair.second) }; + + // Note: this function should ONLY be called for the action map in the user's settings file + // this debug assert should verify that for debug builds + assert(cmdImpl->Origin() == OriginTag::User); + + if (cmdImpl->ID().empty()) + { + fixedUp = cmdImpl->GenerateID() || fixedUp; + } + } + return fixedUp; + } + // Method Description: // - Rebinds a key binding to a new key chord // Arguments: diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index de9b9ca236..880b9989ff 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -71,6 +71,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value ToJson() const; // modification + bool GenerateIDsForActions(); bool RebindKeys(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys); void DeleteKeyBinding(const Control::KeyChord& keys); void RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 93af43319b..37112fe76b 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -504,6 +504,10 @@ bool SettingsLoader::FixupUserSettings() fixedUp = true; } + // we need to generate an ID for a command in the user settings if it doesn't already have one + auto actionMap{ winrt::get_self(userSettings.globals->ActionMap()) }; + fixedUp = actionMap->GenerateIDsForActions() || fixedUp; + return fixedUp; } diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index 91082a4f6d..c20be960a6 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -21,6 +21,7 @@ namespace winrt } static constexpr std::string_view NameKey{ "name" }; +static constexpr std::string_view IDKey{ "id" }; static constexpr std::string_view IconKey{ "icon" }; static constexpr std::string_view ActionKey{ "command" }; static constexpr std::string_view IterateOnKey{ "iterateOn" }; @@ -39,7 +40,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto command{ winrt::make_self() }; command->_name = _name; - command->_Origin = OriginTag::User; + command->_Origin = _Origin; + command->_ID = _ID; command->_ActionAndArgs = *get_self(_ActionAndArgs)->Copy(); command->_keyMappings = _keyMappings; command->_iconPath = _iconPath; @@ -114,6 +116,25 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } } + hstring Command::ID() const noexcept + { + return hstring{ _ID }; + } + + bool Command::GenerateID() + { + if (_ActionAndArgs) + { + auto actionAndArgsImpl{ winrt::get_self(_ActionAndArgs) }; + if (const auto generatedID = actionAndArgsImpl->GenerateID(); !generatedID.empty()) + { + _ID = generatedID; + return true; + } + } + return false; + } + void Command::Name(const hstring& value) { if (!_name.has_value() || _name.value() != value) @@ -264,6 +285,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto result = winrt::make_self(); result->_Origin = origin; + JsonUtils::GetValueForKey(json, IDKey, result->_ID); auto nested = false; JsonUtils::GetValueForKey(json, IterateOnKey, result->_IterateOn); @@ -423,6 +445,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value cmdJson{ Json::ValueType::objectValue }; JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); + if (!_ID.empty()) + { + JsonUtils::SetValueForKey(cmdJson, IDKey, _ID); + } if (_ActionAndArgs) { @@ -443,6 +469,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // First iteration also writes icon and name JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); + if (!_ID.empty()) + { + JsonUtils::SetValueForKey(cmdJson, IDKey, _ID); + } } if (_ActionAndArgs) diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index 5e94ae721c..f22e35348c 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -61,6 +61,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation hstring Name() const noexcept; void Name(const hstring& name); + hstring ID() const noexcept; + bool GenerateID(); + Control::KeyChord Keys() const noexcept; hstring KeyChordText() const noexcept; std::vector KeyMappings() const noexcept; @@ -84,6 +87,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMap _subcommands{ nullptr }; std::vector _keyMappings; std::optional _name; + std::wstring _ID; std::optional _iconPath; bool _nestedCommand{ false }; diff --git a/src/cascadia/TerminalSettingsModel/Command.idl b/src/cascadia/TerminalSettingsModel/Command.idl index c9cec5012a..aa23458f55 100644 --- a/src/cascadia/TerminalSettingsModel/Command.idl +++ b/src/cascadia/TerminalSettingsModel/Command.idl @@ -36,6 +36,7 @@ namespace Microsoft.Terminal.Settings.Model Command(); String Name { get; }; + String ID { get; }; ActionAndArgs ActionAndArgs { get; }; Microsoft.Terminal.Control.KeyChord Keys { get; }; void RegisterKey(Microsoft.Terminal.Control.KeyChord keys); diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 7c33e16999..2c79c63b7c 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -423,145 +423,145 @@ "actions": [ // Application-level Keys - { "command": "closeWindow", "keys": "alt+f4" }, - { "command": "toggleFullscreen", "keys": "alt+enter" }, - { "command": "toggleFullscreen", "keys": "f11" }, - { "command": "toggleFocusMode" }, - { "command": "toggleAlwaysOnTop" }, - { "command": "openNewTabDropdown", "keys": "ctrl+shift+space" }, - { "command": { "action": "openSettings", "target": "settingsUI" }, "keys": "ctrl+," }, - { "command": { "action": "openSettings", "target": "settingsFile" }, "keys": "ctrl+shift+," }, - { "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," }, - { "command": "find", "keys": "ctrl+shift+f" }, - { "command": { "action": "findMatch", "direction": "next" } }, - { "command": { "action": "findMatch", "direction": "prev" } }, - { "command": "toggleShaderEffects" }, - { "command": "openTabColorPicker" }, - { "command": "renameTab" }, - { "command": "openTabRenamer" }, - { "command": "commandPalette", "keys":"ctrl+shift+p" }, - { "command": "identifyWindow" }, - { "command": "openWindowRenamer" }, - { "command": "quakeMode", "keys":"win+sc(41)" }, - { "command": "openSystemMenu", "keys": "alt+space" }, - { "command": "quit" }, - { "command": "restoreLastClosed" }, - { "command": "openAbout" }, + { "command": "closeWindow", "keys": "alt+f4", "id": "Terminal.CloseWindow" }, + { "command": "toggleFullscreen", "keys": "alt+enter", "id": "Terminal.ToggleFullscreen" }, + { "command": "toggleFullscreen", "keys": "f11", "id": "Terminal.ToggleFullscreen" }, + { "command": "toggleFocusMode", "id": "Terminal.ToggleFocusMode" }, + { "command": "toggleAlwaysOnTop", "id": "Terminal.ToggleAlwaysOnTop" }, + { "command": "openNewTabDropdown", "keys": "ctrl+shift+space", "id": "Terminal.OpenNewTabDropdown" }, + { "command": { "action": "openSettings", "target": "settingsUI" }, "keys": "ctrl+,", "id": "Terminal.OpenSettingsUI" }, + { "command": { "action": "openSettings", "target": "settingsFile" }, "keys": "ctrl+shift+,", "id": "Terminal.OpenSettingsFile" }, + { "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+,", "id": "Terminal.OpenDefaultSettingsFile" }, + { "command": "find", "keys": "ctrl+shift+f", "id": "Terminal.FindText" }, + { "command": { "action": "findMatch", "direction": "next" }, "id": "Terminal.FindNextMatch" }, + { "command": { "action": "findMatch", "direction": "prev" }, "id": "Terminal.FindPrevMatch" }, + { "command": "toggleShaderEffects", "id": "Terminal.ToggleShaderEffects" }, + { "command": "openTabColorPicker", "id": "Terminal.OpenTabColorPicker" }, + { "command": "renameTab", "id": "Terminal.RenameTab" }, + { "command": "openTabRenamer", "id": "Terminal.OpenTabRenamer" }, + { "command": "commandPalette", "keys":"ctrl+shift+p", "id": "Terminal.ToggleCommandPalette" }, + { "command": "identifyWindow", "id": "Terminal.IdentifyWindow" }, + { "command": "openWindowRenamer", "id": "Terminal.OpenWindowRenamer" }, + { "command": "quakeMode", "keys":"win+sc(41)", "id": "Terminal.QuakeMode" }, + { "command": "openSystemMenu", "keys": "alt+space", "id": "Terminal.OpenSystemMenu" }, + { "command": "quit", "id": "Terminal.Quit" }, + { "command": "restoreLastClosed", "id": "Terminal.RestoreLastClosed" }, + { "command": "openAbout", "id": "Terminal.OpenAboutDialog" }, // Tab Management // "command": "closeTab" is unbound by default. // The closeTab command closes a tab without confirmation, even if it has multiple panes. - { "command": "closeOtherTabs" }, - { "command": "closeTabsAfter" }, - { "command": { "action" : "moveTab", "direction": "forward" }}, - { "command": { "action" : "moveTab", "direction": "backward" }}, - { "command": "newTab", "keys": "ctrl+shift+t" }, - { "command": "newWindow", "keys": "ctrl+shift+n" }, - { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+shift+1" }, - { "command": { "action": "newTab", "index": 1 }, "keys": "ctrl+shift+2" }, - { "command": { "action": "newTab", "index": 2 }, "keys": "ctrl+shift+3" }, - { "command": { "action": "newTab", "index": 3 }, "keys": "ctrl+shift+4" }, - { "command": { "action": "newTab", "index": 4 }, "keys": "ctrl+shift+5" }, - { "command": { "action": "newTab", "index": 5 }, "keys": "ctrl+shift+6" }, - { "command": { "action": "newTab", "index": 6 }, "keys": "ctrl+shift+7" }, - { "command": { "action": "newTab", "index": 7 }, "keys": "ctrl+shift+8" }, - { "command": { "action": "newTab", "index": 8 }, "keys": "ctrl+shift+9" }, - { "command": "duplicateTab", "keys": "ctrl+shift+d" }, - { "command": "nextTab", "keys": "ctrl+tab" }, - { "command": "prevTab", "keys": "ctrl+shift+tab" }, - { "command": { "action": "switchToTab", "index": 0 }, "keys": "ctrl+alt+1" }, - { "command": { "action": "switchToTab", "index": 1 }, "keys": "ctrl+alt+2" }, - { "command": { "action": "switchToTab", "index": 2 }, "keys": "ctrl+alt+3" }, - { "command": { "action": "switchToTab", "index": 3 }, "keys": "ctrl+alt+4" }, - { "command": { "action": "switchToTab", "index": 4 }, "keys": "ctrl+alt+5" }, - { "command": { "action": "switchToTab", "index": 5 }, "keys": "ctrl+alt+6" }, - { "command": { "action": "switchToTab", "index": 6 }, "keys": "ctrl+alt+7" }, - { "command": { "action": "switchToTab", "index": 7 }, "keys": "ctrl+alt+8" }, - { "command": { "action": "switchToTab", "index": 4294967295 }, "keys": "ctrl+alt+9" }, - { "command": { "action": "moveTab", "window": "new" }, }, + { "command": "closeOtherTabs", "id": "Terminal.CloseOtherTabs" }, + { "command": "closeTabsAfter", "id": "Terminal.CloseTabsAfter" }, + { "command": { "action" : "moveTab", "direction": "forward" }, "id": "Terminal.MoveTabForward" }, + { "command": { "action" : "moveTab", "direction": "backward" }, "id": "Terminal.MoveTabBackward" }, + { "command": "newTab", "keys": "ctrl+shift+t", "id": "Terminal.OpenNewTab" }, + { "command": "newWindow", "keys": "ctrl+shift+n", "id": "Terminal.OpenNewWindow" }, + { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+shift+1", "id": "Terminal.OpenNewTabProfile0" }, + { "command": { "action": "newTab", "index": 1 }, "keys": "ctrl+shift+2", "id": "Terminal.OpenNewTabProfile1" }, + { "command": { "action": "newTab", "index": 2 }, "keys": "ctrl+shift+3", "id": "Terminal.OpenNewTabProfile2" }, + { "command": { "action": "newTab", "index": 3 }, "keys": "ctrl+shift+4", "id": "Terminal.OpenNewTabProfile3" }, + { "command": { "action": "newTab", "index": 4 }, "keys": "ctrl+shift+5", "id": "Terminal.OpenNewTabProfile4" }, + { "command": { "action": "newTab", "index": 5 }, "keys": "ctrl+shift+6", "id": "Terminal.OpenNewTabProfile5" }, + { "command": { "action": "newTab", "index": 6 }, "keys": "ctrl+shift+7", "id": "Terminal.OpenNewTabProfile6" }, + { "command": { "action": "newTab", "index": 7 }, "keys": "ctrl+shift+8", "id": "Terminal.OpenNewTabProfile7" }, + { "command": { "action": "newTab", "index": 8 }, "keys": "ctrl+shift+9", "id": "Terminal.OpenNewTabProfile8" }, + { "command": "duplicateTab", "keys": "ctrl+shift+d", "id": "Terminal.DuplicateTab" }, + { "command": "nextTab", "keys": "ctrl+tab", "id": "Terminal.NextTab" }, + { "command": "prevTab", "keys": "ctrl+shift+tab", "id": "Terminal.PrevTab" }, + { "command": { "action": "switchToTab", "index": 0 }, "keys": "ctrl+alt+1", "id": "Terminal.SwitchToTab0" }, + { "command": { "action": "switchToTab", "index": 1 }, "keys": "ctrl+alt+2", "id": "Terminal.SwitchToTab1" }, + { "command": { "action": "switchToTab", "index": 2 }, "keys": "ctrl+alt+3", "id": "Terminal.SwitchToTab2" }, + { "command": { "action": "switchToTab", "index": 3 }, "keys": "ctrl+alt+4", "id": "Terminal.SwitchToTab3" }, + { "command": { "action": "switchToTab", "index": 4 }, "keys": "ctrl+alt+5", "id": "Terminal.SwitchToTab4" }, + { "command": { "action": "switchToTab", "index": 5 }, "keys": "ctrl+alt+6", "id": "Terminal.SwitchToTab5" }, + { "command": { "action": "switchToTab", "index": 6 }, "keys": "ctrl+alt+7", "id": "Terminal.SwitchToTab6" }, + { "command": { "action": "switchToTab", "index": 7 }, "keys": "ctrl+alt+8", "id": "Terminal.SwitchToTab7" }, + { "command": { "action": "switchToTab", "index": 4294967295 }, "keys": "ctrl+alt+9", "id": "Terminal.SwitchToLastTab" }, + { "command": { "action": "moveTab", "window": "new" }, "id": "Terminal.MoveTabToNewWindow" }, // Pane Management - { "command": "closeOtherPanes" }, - { "command": "closePane", "keys": "ctrl+shift+w" }, - { "command": { "action": "splitPane", "split": "up" } }, - { "command": { "action": "splitPane", "split": "down" } }, - { "command": { "action": "splitPane", "split": "left" } }, - { "command": { "action": "splitPane", "split": "right" } }, - { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "down" }, "keys": "alt+shift+-" }, - { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "right" }, "keys": "alt+shift+plus" }, - { "command": { "action": "resizePane", "direction": "down" }, "keys": "alt+shift+down" }, - { "command": { "action": "resizePane", "direction": "left" }, "keys": "alt+shift+left" }, - { "command": { "action": "resizePane", "direction": "right" }, "keys": "alt+shift+right" }, - { "command": { "action": "resizePane", "direction": "up" }, "keys": "alt+shift+up" }, - { "command": { "action": "moveFocus", "direction": "down" }, "keys": "alt+down" }, - { "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left" }, - { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right" }, - { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up" }, - { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left"}, - { "command": { "action": "moveFocus", "direction": "previousInOrder" } }, - { "command": { "action": "moveFocus", "direction": "nextInOrder" } }, - { "command": { "action": "moveFocus", "direction": "first" } }, - { "command": { "action": "moveFocus", "direction": "parent" } }, - { "command": { "action": "moveFocus", "direction": "child" } }, - { "command": { "action": "swapPane", "direction": "down" } }, - { "command": { "action": "swapPane", "direction": "left" } }, - { "command": { "action": "swapPane", "direction": "right" } }, - { "command": { "action": "swapPane", "direction": "up" } }, - { "command": { "action": "swapPane", "direction": "previous"} }, - { "command": { "action": "swapPane", "direction": "previousInOrder"} }, - { "command": { "action": "swapPane", "direction": "nextInOrder"} }, - { "command": { "action": "swapPane", "direction": "first" } }, - { "command": "toggleBroadcastInput" }, - { "command": "togglePaneZoom" }, - { "command": "toggleSplitOrientation" }, - { "command": "toggleReadOnlyMode" }, - { "command": "enableReadOnlyMode" }, - { "command": "disableReadOnlyMode" }, - { "command": { "action": "movePane", "index": 0 } }, - { "command": { "action": "movePane", "index": 1 } }, - { "command": { "action": "movePane", "index": 2 } }, - { "command": { "action": "movePane", "index": 3 } }, - { "command": { "action": "movePane", "index": 4 } }, - { "command": { "action": "movePane", "index": 5 } }, - { "command": { "action": "movePane", "index": 6 } }, - { "command": { "action": "movePane", "index": 7 } }, - { "command": { "action": "movePane", "index": 8 } }, - { "command": { "action": "movePane", "window": "new" }, }, - { "command": "restartConnection" }, + { "command": "closeOtherPanes", "id": "Terminal.CloseOtherPanes" }, + { "command": "closePane", "keys": "ctrl+shift+w", "id": "Terminal.ClosePane" }, + { "command": { "action": "splitPane", "split": "up" }, "id": "Terminal.SplitPaneUp" }, + { "command": { "action": "splitPane", "split": "down" }, "id": "Terminal.SplitPaneDown" }, + { "command": { "action": "splitPane", "split": "left" }, "id": "Terminal.SplitPaneLeft" }, + { "command": { "action": "splitPane", "split": "right" }, "id": "Terminal.SplitPaneRight" }, + { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "down" }, "keys": "alt+shift+-", "id": "Terminal.SplitPaneDuplicateDown" }, + { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "right" }, "keys": "alt+shift+plus", "id": "Terminal.SplitPaneDuplicateRight" }, + { "command": { "action": "resizePane", "direction": "down" }, "keys": "alt+shift+down", "id": "Terminal.ResizePaneDown" }, + { "command": { "action": "resizePane", "direction": "left" }, "keys": "alt+shift+left", "id": "Terminal.ResizePaneLeft" }, + { "command": { "action": "resizePane", "direction": "right" }, "keys": "alt+shift+right", "id": "Terminal.ResizePaneRight" }, + { "command": { "action": "resizePane", "direction": "up" }, "keys": "alt+shift+up", "id": "Terminal.ResizePaneUp" }, + { "command": { "action": "moveFocus", "direction": "down" }, "keys": "alt+down", "id": "Terminal.MoveFocusDown" }, + { "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left", "id": "Terminal.MoveFocusLeft" }, + { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right", "id": "Terminal.MoveFocusRight" }, + { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up", "id": "Terminal.MoveFocusUp" }, + { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left", "id": "Terminal.MoveFocusPrevious" }, + { "command": { "action": "moveFocus", "direction": "previousInOrder" }, "id": "Terminal.MoveFocusPreviousInOrder" }, + { "command": { "action": "moveFocus", "direction": "nextInOrder" }, "id": "Terminal.MoveFocusNextInOrder" }, + { "command": { "action": "moveFocus", "direction": "first" }, "id": "Terminal.MoveFocusFirst" }, + { "command": { "action": "moveFocus", "direction": "parent" }, "id": "Terminal.MoveFocusParent" }, + { "command": { "action": "moveFocus", "direction": "child" }, "id": "Terminal.MoveFocusChild" }, + { "command": { "action": "swapPane", "direction": "down" }, "id": "Terminal.SwapPaneDown" }, + { "command": { "action": "swapPane", "direction": "left" }, "id": "Terminal.SwapPaneLeft" }, + { "command": { "action": "swapPane", "direction": "right" }, "id": "Terminal.SwapPaneRight" }, + { "command": { "action": "swapPane", "direction": "up" }, "id": "Terminal.SwapPaneUp" }, + { "command": { "action": "swapPane", "direction": "previous"}, "id": "Terminal.SwapPanePrevious" }, + { "command": { "action": "swapPane", "direction": "previousInOrder"}, "id": "Terminal.SwapPanePreviousInOrder" }, + { "command": { "action": "swapPane", "direction": "nextInOrder"}, "id": "Terminal.SwapPaneNextInOrder" }, + { "command": { "action": "swapPane", "direction": "first" }, "id": "Terminal.SwapPaneFirst" }, + { "command": "toggleBroadcastInput", "id": "Terminal.ToggleBroadcastInput" }, + { "command": "togglePaneZoom", "id": "Terminal.TogglePaneZoom" }, + { "command": "toggleSplitOrientation", "id": "Terminal.ToggleSplitOrientation" }, + { "command": "toggleReadOnlyMode", "id": "Terminal.ToggleReadOnlyMode" }, + { "command": "enableReadOnlyMode", "id": "Terminal.EnableReadOnlyMode" }, + { "command": "disableReadOnlyMode", "id": "Terminal.DisableReadOnlyMode" }, + { "command": { "action": "movePane", "index": 0 }, "id": "Terminal.MovePaneToTab0" }, + { "command": { "action": "movePane", "index": 1 }, "id": "Terminal.MovePaneToTab1" }, + { "command": { "action": "movePane", "index": 2 }, "id": "Terminal.MovePaneToTab2" }, + { "command": { "action": "movePane", "index": 3 }, "id": "Terminal.MovePaneToTab3" }, + { "command": { "action": "movePane", "index": 4 }, "id": "Terminal.MovePaneToTab4" }, + { "command": { "action": "movePane", "index": 5 }, "id": "Terminal.MovePaneToTab5" }, + { "command": { "action": "movePane", "index": 6 }, "id": "Terminal.MovePaneToTab6" }, + { "command": { "action": "movePane", "index": 7 }, "id": "Terminal.MovePaneToTab7" }, + { "command": { "action": "movePane", "index": 8 }, "id": "Terminal.MovePaneToTab8" }, + { "command": { "action": "movePane", "window": "new" }, "id": "Terminal.MovePaneToNewWindow" }, + { "command": "restartConnection", "id": "Terminal.RestartConnection" }, // Clipboard Integration - { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c" }, - { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+insert" }, - { "command": { "action": "copy", "singleLine": false }, "keys": "enter" }, - { "command": "paste", "keys": "ctrl+shift+v" }, - { "command": "paste", "keys": "shift+insert" }, - { "command": "selectAll", "keys": "ctrl+shift+a" }, - { "command": "markMode", "keys": "ctrl+shift+m" }, - { "command": "toggleBlockSelection" }, - { "command": "switchSelectionEndpoint" }, - { "command": "expandSelectionToWord" }, - { "command": "showContextMenu", "keys": "menu" }, + { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c", "id": "Terminal.CopySelectedText" }, + { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+insert", "id": "Terminal.CopySelectedText" }, + { "command": { "action": "copy", "singleLine": false }, "keys": "enter", "id": "Terminal.CopySelectedText" }, + { "command": "paste", "keys": "ctrl+shift+v", "id": "Terminal.PasteFromClipboard" }, + { "command": "paste", "keys": "shift+insert", "id": "Terminal.PasteFromClipboard" }, + { "command": "selectAll", "keys": "ctrl+shift+a", "id": "Terminal.SelectAll" }, + { "command": "markMode", "keys": "ctrl+shift+m", "id": "Terminal.ToggleMarkMode" }, + { "command": "toggleBlockSelection", "id": "Terminal.ToggleBlockSelection" }, + { "command": "switchSelectionEndpoint", "id": "Terminal.SwitchSelectionEndpoint" }, + { "command": "expandSelectionToWord", "id": "Terminal.ExpandSelectionToWord" }, + { "command": "showContextMenu", "keys": "menu", "id": "Terminal.ShowContextMenu" }, // Web Search - { "command": { "action": "searchWeb" }, "name": { "key": "SearchWebCommandKey" } }, + { "command": { "action": "searchWeb" }, "name": { "key": "SearchWebCommandKey" }, "id": "Terminal.SearchWeb" }, // Scrollback - { "command": "scrollDown", "keys": "ctrl+shift+down" }, - { "command": "scrollDownPage", "keys": "ctrl+shift+pgdn" }, - { "command": "scrollUp", "keys": "ctrl+shift+up" }, - { "command": "scrollUpPage", "keys": "ctrl+shift+pgup" }, - { "command": "scrollToTop", "keys": "ctrl+shift+home" }, - { "command": "scrollToBottom", "keys": "ctrl+shift+end" }, - { "command": { "action": "clearBuffer", "clear": "all" } }, - { "command": "exportBuffer" }, + { "command": "scrollDown", "keys": "ctrl+shift+down", "id": "Terminal.ScrollDown" }, + { "command": "scrollDownPage", "keys": "ctrl+shift+pgdn", "id": "Terminal.ScrollDownPage" }, + { "command": "scrollUp", "keys": "ctrl+shift+up", "id": "Terminal.ScrollUp" }, + { "command": "scrollUpPage", "keys": "ctrl+shift+pgup", "id": "Terminal.ScrollUpPage" }, + { "command": "scrollToTop", "keys": "ctrl+shift+home", "id": "Terminal.ScrollToTop" }, + { "command": "scrollToBottom", "keys": "ctrl+shift+end", "id": "Terminal.ScrollToBottom" }, + { "command": { "action": "clearBuffer", "clear": "all" }, "id": "Terminal.ClearBuffer" }, + { "command": "exportBuffer", "id": "Terminal.ExportBuffer" }, // Visual Adjustments - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" }, - { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus" }, - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+numpad_plus" }, - { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+numpad_minus" }, - { "command": "resetFontSize", "keys": "ctrl+0" }, - { "command": "resetFontSize", "keys": "ctrl+numpad_0" }, + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus", "id": "Terminal.IncreaseFontSize" }, + { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus", "id": "Terminal.DecreaseFontSize" }, + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+numpad_plus", "id": "Terminal.IncreaseFontSize" }, + { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+numpad_minus", "id": "Terminal.DecreaseFontSize" }, + { "command": "resetFontSize", "keys": "ctrl+0", "id": "Terminal.ResetFontSize" }, + { "command": "resetFontSize", "keys": "ctrl+numpad_0", "id": "Terminal.ResetFontSize" }, // Other commands { diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index 8cfd4a14cc..8d9c55b6f8 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -16,6 +16,14 @@ using namespace WEX::Common; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::Control; +// Different architectures will hash the same SendInput command to a different ID +// Check for the correct ID based on the architecture +#if defined(_M_IX86) +#define SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH "56911147" +#else +#define SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH "A020D2" +#endif + namespace SettingsModelUnitTests { class SerializationTests : public JsonTestClass @@ -36,6 +44,10 @@ namespace SettingsModelUnitTests TEST_METHOD(RoundtripUserModifiedColorSchemeCollisionUnusedByProfiles); TEST_METHOD(RoundtripUserDeletedColorSchemeCollision); + TEST_METHOD(RoundtripGenerateActionID); + TEST_METHOD(NoGeneratedIDsForIterableAndNestedCommands); + TEST_METHOD(GeneratedActionIDsEqualForIdenticalCommands); + private: // Method Description: // - deserializes and reserializes a json string representing a settings object model of type T @@ -491,7 +503,7 @@ namespace SettingsModelUnitTests } ], "actions": [ - { "command": { "action": "sendInput", "input": "VT Griese Mode" }, "keys": "ctrl+k" } + { "command": { "action": "sendInput", "input": "VT Griese Mode" }, "id": "User.sendInput.E02B3DF9", "keys": "ctrl+k" } ], "theme": "system", "themes": [] @@ -946,4 +958,127 @@ namespace SettingsModelUnitTests VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); } + + void SerializationTests::RoundtripGenerateActionID() + { + static constexpr std::string_view oldSettingsJson{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "just some input" }, + "keys": "ctrl+shift+w" + } + ] + })" }; + + // Key differences: - the sendInput action now has a generated ID + // - this generated ID was created at the time of writing this test, + // and should remain robust (i.e. every time we hash the args we should get the same result) + static constexpr std::string_view newSettingsJson{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "just some input" }, + "keys": "ctrl+shift+w", + "id" : "User.sendInput.)" SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH R"(" + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + VERIFY_IS_TRUE(oldLoader.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto oldSettings = winrt::make_self(std::move(oldLoader)); + const auto oldResult{ oldSettings->ToJson() }; + + implementation::SettingsLoader newLoader{ newSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; + newLoader.MergeInboxIntoUserSettings(); + newLoader.FinalizeLayering(); + newLoader.FixupUserSettings(); + const auto newSettings = winrt::make_self(std::move(newLoader)); + const auto newResult{ newSettings->ToJson() }; + + VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); + } + + void SerializationTests::NoGeneratedIDsForIterableAndNestedCommands() + { + // for iterable commands, nested commands, and user-defined actions that already have + // an ID, we do not need to generate an ID + static constexpr std::string_view oldSettingsJson{ R"( + { + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w", + "id": "thisIsMyClosePane" + }, + { + "iterateOn": "profiles", + "icon": "${profile.icon}", + "name": "${profile.name}", + "command": { "action": "newTab", "profile": "${profile.name}" } + }, + { + "name": "Change font size...", + "commands": [ + { "command": { "action": "adjustFontSize", "delta": 1.0 } }, + { "command": { "action": "adjustFontSize", "delta": -1.0 } }, + { "command": "resetFontSize" }, + ] + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + VERIFY_IS_FALSE(oldLoader.FixupUserSettings(), L"Validate that there is no need to write back to disk"); + } + + void SerializationTests::GeneratedActionIDsEqualForIdenticalCommands() + { + static constexpr std::string_view settingsJson1{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "this is some other input string" }, + "keys": "ctrl+shift+w" + } + ] + })" }; + + // Both settings files define the same action, so the generated ID should be the same for both + static constexpr std::string_view settingsJson2{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "this is some other input string" }, + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader loader1{ settingsJson1, implementation::LoadStringResource(IDR_DEFAULTS) }; + loader1.MergeInboxIntoUserSettings(); + loader1.FinalizeLayering(); + VERIFY_IS_TRUE(loader1.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto settings1 = winrt::make_self(std::move(loader1)); + const auto result1{ settings1->ToJson() }; + + implementation::SettingsLoader loader2{ settingsJson2, implementation::LoadStringResource(IDR_DEFAULTS) }; + loader2.MergeInboxIntoUserSettings(); + loader2.FinalizeLayering(); + VERIFY_IS_TRUE(loader2.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto settings2 = winrt::make_self(std::move(loader2)); + const auto result2{ settings2->ToJson() }; + + VERIFY_ARE_EQUAL(toString(result1), toString(result2)); + } } From 4e7b63c664bc09da3ba2f08287cf03d3729ce3a2 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 18 Apr 2024 19:47:28 +0200 Subject: [PATCH 07/53] A minor TSF refactoring (#17067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next in the popular series of minor refactorings: Out with the old, in with the new! This PR removes all of the existing TSF code, both for conhost and Windows Terminal. conhost's TSF implementation was awful: It allocated an entire text buffer _per line_ of input. Additionally, its implementation spanned a whopping 40 files and almost 5000 lines of code. Windows Terminal's implementation was absolutely fine in comparison, but it was user unfriendly due to two reasons: Its usage of the `CoreTextServices` WinRT API indirectly meant that it used a non-transitory TSF document, which is not the right choice for a terminal. A `TF_SS_TRANSITORY` document (-context) indicates to TSF that it cannot undo a previously completed composition which is exactly what we need: Once composition has completed we send the result to the shell and we cannot undo this later on. The WinRT API does not allow us to use `TF_SS_TRANSITORY` and so it's unsuitable for our application. Additionally, the implementation used XAML to render the composition instead of being part of our text renderer, which resulted in the text looking weird and hard to read. The new implementation spans just 8 files and is ~1000 lines which should make it significantly easier to maintain. The architecture is not particularly great, but it's certainly better than what we had. The implementation is almost entirely identical between both conhost and Windows Terminal and thus they both also behave identical. It fixes an uncountable number of subtle bugs in the conhost TSF implementation, as it failed to check for status codes after calls. It also adds several new features, like support for wavy underlines (as used by the Japanese IME), dashed underlines (the default for various languages now, like Vietnamese), colored underlines, colored foreground/background controlled by the IME, and more! I have tried to replicate the following issues and have a high confidence that they're resolved now: Closes #1304 Closes #3730 Closes #4052 Closes #5007 (as it is not applicable anymore) Closes #5110 Closes #6186 Closes #6192 Closes #13805 Closes #14349 Closes #14407 Closes #16180 For the following issues I'm not entirely sure if it'll fix it, but I suspect it's somewhat likely: #13681 #16305 #16817 Lastly, there's one remaining bug that I don't know how to resolve. However, that issue also plagues conhost and Windows Terminal right now, so it's at least not a regression: * Press Win+. (emoji picker) and close it * Move the window around * Press Win+. This will open the emoji picker at the old window location. It also occurs when the cursor moves within the window. While this is super annoying, I could not find a way to fix it. ## Validation Steps Performed * See the above closed issues * Use Vietnamese Telex and type "xin choaf" Results in "xin chào" ✅ * Use the MS Japanese IME and press Alt+` Toggles between the last 2 modes ✅ * Use the MS Japanese IME, type "kyouhaishaheiku", and press Space * The text is converted, underlined and the first part is doubly underlined ✅ * Left/Right moves between the 3 segments ✅ * Home/End moves between start/end ✅ * Esc puts a wavy line under the current segment ✅ * Use the Korean IME, type "gksgks" This results in "한한" ✅ * Use the Korean IME, type "gks", and press Right Ctrl Opens a popup which allows you to navigate with Arrow/Tab keys ✅ --- .github/actions/spelling/expect/alphabet.txt | 1 - .github/actions/spelling/expect/expect.txt | 64 +- OpenConsole.sln | 6 +- doc/ORGANIZATION.md | 3 - src/buffer/out/Row.cpp | 8 +- src/buffer/out/Row.hpp | 2 - src/buffer/out/textBuffer.hpp | 2 - src/cascadia/TerminalControl/ControlCore.cpp | 44 +- src/cascadia/TerminalControl/ControlCore.h | 10 +- src/cascadia/TerminalControl/ControlCore.idl | 2 - .../TerminalControl/TSFInputControl.cpp | 440 ------- .../TerminalControl/TSFInputControl.h | 92 -- .../TerminalControl/TSFInputControl.idl | 36 - .../TerminalControl/TSFInputControl.xaml | 17 - src/cascadia/TerminalControl/TermControl.cpp | 251 ++-- src/cascadia/TerminalControl/TermControl.h | 46 +- src/cascadia/TerminalControl/TermControl.xaml | 5 - .../TerminalControlLib.vcxproj | 29 +- src/cascadia/TerminalCore/Terminal.cpp | 29 - src/cascadia/TerminalCore/Terminal.hpp | 8 +- .../TerminalCore/terminalrenderdata.cpp | 7 +- .../TerminalBufferTests.cpp | 42 - src/host/_output.cpp | 2 - src/host/conareainfo.cpp | 221 ---- src/host/conareainfo.h | 74 -- src/host/conimeinfo.cpp | 507 ------- src/host/conimeinfo.h | 91 -- src/host/consoleInformation.cpp | 2 - src/host/conv.h | 29 - src/host/convarea.cpp | 163 --- src/host/getset.cpp | 5 - src/host/globals.h | 4 +- src/host/host-common.vcxitems | 6 - src/host/inputBuffer.cpp | 2 - src/host/inputBuffer.hpp | 1 - src/host/lib/hostlib.vcxproj.filters | 23 +- src/host/output.cpp | 2 - src/host/precomp.h | 3 - src/host/renderData.cpp | 45 +- src/host/renderData.hpp | 4 +- src/host/screenInfo.cpp | 12 - src/host/screenInfo.hpp | 3 - src/host/server.h | 3 - src/host/sources.inc | 3 - src/host/ut_host/VtIoTests.cpp | 7 +- src/inc/conime.h | 58 - src/inc/contsf.h | 40 - .../win32/CustomWindowMessages.h | 2 +- src/interactivity/win32/WindowIme.cpp | 68 - src/interactivity/win32/lib/win32.LIB.vcxproj | 2 - .../win32/lib/win32.LIB.vcxproj.filters | 6 - src/interactivity/win32/menu.cpp | 2 - src/interactivity/win32/sources.inc | 1 - src/interactivity/win32/window.cpp | 6 - src/interactivity/win32/windowime.hpp | 9 - src/interactivity/win32/windowio.cpp | 22 +- src/interactivity/win32/windowproc.cpp | 129 +- src/renderer/atlas/AtlasEngine.cpp | 1 + src/renderer/base/renderer.cpp | 210 +-- src/renderer/base/renderer.hpp | 13 +- src/renderer/inc/IRenderData.hpp | 32 +- src/tsf/ConsoleTSF.cpp | 549 -------- src/tsf/ConsoleTSF.h | 220 ---- src/tsf/Handle.cpp | 82 ++ src/tsf/Handle.h | 54 + src/tsf/Implementation.cpp | 663 ++++++++++ src/tsf/Implementation.h | 108 ++ src/tsf/TfCatUtil.cpp | 59 - src/tsf/TfCatUtil.h | 38 - src/tsf/TfConvArea.cpp | 103 -- src/tsf/TfConvArea.h | 44 - src/tsf/TfCtxtComp.h | 44 - src/tsf/TfDispAttr.cpp | 199 --- src/tsf/TfDispAttr.h | 51 - src/tsf/TfEditSession.cpp | 1167 ----------------- src/tsf/TfEditSession.h | 219 ---- src/tsf/TfTxtevCb.cpp | 169 --- src/tsf/contsf.cpp | 33 - src/tsf/globals.h | 57 - src/tsf/precomp.h | 9 +- src/tsf/sources | 11 +- src/tsf/tsf.vcxproj | 21 +- src/tsf/tsf.vcxproj.filters | 55 +- 83 files changed, 1413 insertions(+), 5499 deletions(-) delete mode 100644 src/cascadia/TerminalControl/TSFInputControl.cpp delete mode 100644 src/cascadia/TerminalControl/TSFInputControl.h delete mode 100644 src/cascadia/TerminalControl/TSFInputControl.idl delete mode 100644 src/cascadia/TerminalControl/TSFInputControl.xaml delete mode 100644 src/host/conareainfo.cpp delete mode 100644 src/host/conareainfo.h delete mode 100644 src/host/conimeinfo.cpp delete mode 100644 src/host/conimeinfo.h delete mode 100644 src/host/conv.h delete mode 100644 src/host/convarea.cpp delete mode 100644 src/inc/conime.h delete mode 100644 src/inc/contsf.h delete mode 100644 src/interactivity/win32/WindowIme.cpp delete mode 100644 src/interactivity/win32/windowime.hpp delete mode 100644 src/tsf/ConsoleTSF.cpp delete mode 100644 src/tsf/ConsoleTSF.h create mode 100644 src/tsf/Handle.cpp create mode 100644 src/tsf/Handle.h create mode 100644 src/tsf/Implementation.cpp create mode 100644 src/tsf/Implementation.h delete mode 100644 src/tsf/TfCatUtil.cpp delete mode 100644 src/tsf/TfCatUtil.h delete mode 100644 src/tsf/TfConvArea.cpp delete mode 100644 src/tsf/TfConvArea.h delete mode 100644 src/tsf/TfCtxtComp.h delete mode 100644 src/tsf/TfDispAttr.cpp delete mode 100644 src/tsf/TfDispAttr.h delete mode 100644 src/tsf/TfEditSession.cpp delete mode 100644 src/tsf/TfEditSession.h delete mode 100644 src/tsf/TfTxtevCb.cpp delete mode 100644 src/tsf/contsf.cpp delete mode 100644 src/tsf/globals.h diff --git a/.github/actions/spelling/expect/alphabet.txt b/.github/actions/spelling/expect/alphabet.txt index 97180b7211..6a2ee3bb51 100644 --- a/.github/actions/spelling/expect/alphabet.txt +++ b/.github/actions/spelling/expect/alphabet.txt @@ -1,4 +1,3 @@ -AAAa AAAAA AAAAAAAAAAAAA AAAAAABBBBBBCCC diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index e458086ce8..36e345cfbb 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -10,6 +10,7 @@ ACCESSTOKEN acidev ACIOSS ACover +acp actctx ACTCTXW ADDALIAS @@ -67,10 +68,10 @@ ASBSET asdfghjkl ASetting ASingle +ASYNCDONTCARE ASYNCWINDOWPOS atch ATest -ATTRCOLOR aumid Authenticode AUTOBUDDY @@ -124,8 +125,6 @@ Blt BLUESCROLL BODGY BOLDFONT -BOOLIFY -bools Borland boutput boxheader @@ -156,7 +155,6 @@ cazamor CBash cbiex CBN -CBoolean cbt CCCBB cch @@ -164,12 +162,9 @@ CCHAR CCmd ccolor CCom -CComp CConsole -CConversion CCRT cdd -CEdit CELLSIZE cfae cfie @@ -179,18 +174,17 @@ CFuzz cgscrn chafa changelists -chaof charinfo CHARSETINFO chh chshdng CHT -Cic CLASSSTRING CLE cleartype CLICKACTIVE clickdown +CLIENTID clipbrd CLIPCHILDREN CLIPSIBLINGS @@ -229,7 +223,6 @@ commdlg COMMITID componentization conapi -conareainfo conattrs conbufferout concfg @@ -240,8 +233,7 @@ condrv conechokey conemu conhost -conime -conimeinfo +CONIME conintegrity conintegrityuwp coninteractivitybase @@ -279,7 +271,6 @@ contentfiles conterm contsf contypes -convarea conwinuserrefs coordnew COPYCOLOR @@ -314,7 +305,6 @@ csrutil CSTYLE CSwitch CTerminal -CText ctl ctlseqs CTRLEVENT @@ -322,7 +312,7 @@ CTRLFREQUENCY CTRLKEYSHORTCUTS Ctrls CTRLVOLUME -Ctxt +CUAS CUF cupxy CURRENTFONT @@ -486,7 +476,6 @@ DISABLEDELAYEDEXPANSION DISABLENOSCROLL DISPATCHNOTIFY DISPLAYATTRIBUTE -DISPLAYATTRIBUTEPROPERTY DISPLAYCHANGE distros dlg @@ -562,7 +551,6 @@ entrypoints ENU ENUMLOGFONT ENUMLOGFONTEX -enumranges EOK EPres EQU @@ -621,7 +609,6 @@ FINDDOWN FINDSTRINGEXACT FINDUP FIter -FIXEDCONVERTED FIXEDFILEINFO Flg flyouts @@ -736,9 +723,8 @@ Greyscale gridline gset gsl -GTP guc -guidatom +GUIDATOM GValue GWL GWLP @@ -837,7 +823,6 @@ IEnd IEnum IFACEMETHODIMP ification -IGNOREEND IGNORELANGUAGE IHosted iid @@ -861,7 +846,6 @@ inkscape INLINEPREFIX inproc Inputkeyinfo -INPUTPROCESSORPROFILE Inputreadhandledata INSERTMODE INTERACTIVITYBASE @@ -873,9 +857,7 @@ INVALIDARG INVALIDATERECT Ioctl ipch -ipp IProperty -IPSINK ipsp IShell ISwap @@ -895,7 +877,6 @@ JOBOBJECT JOBOBJECTINFOCLASS JPN jsoncpp -Jsons jsprovider jumplist KAttrs @@ -925,6 +906,7 @@ KLMNOPQRSTY KOK KPRIORITY KVM +kyouhaishaheiku langid LANGUAGELIST lasterror @@ -987,7 +969,6 @@ lpdw lpelfe lpfn LPFNADDPROPSHEETPAGE -lpl LPMEASUREITEMSTRUCT LPMINMAXINFO lpmsg @@ -1063,6 +1044,7 @@ MENUITEMINFO MENUSELECT messageext metaproj +Mgrs microsoftpublicsymbols midl mii @@ -1117,7 +1099,6 @@ muxes myapplet mybranch mydir -MYMAX Mypair Myval NAMELENGTH @@ -1161,6 +1142,7 @@ NOCOPYBITS NODUP noexcepts NOFONT +NOHIDDENTEXT NOINTEGRALHEIGHT NOINTERFACE NOLINKINFO @@ -1184,6 +1166,7 @@ NORMALDISPLAY NOSCRATCH NOSEARCH noselect +NOSELECTION NOSENDCHANGING NOSIZE NOSNAPSHOT @@ -1274,6 +1257,7 @@ packageuwp PACKAGEVERSIONNUMBER PACKCOORD PACKVERSION +pacp pagedown pageup PAINTPARAMS @@ -1285,7 +1269,6 @@ parms PATCOPY pathcch PATTERNID -pcat pcb pcch PCCHAR @@ -1309,9 +1292,9 @@ PCSTR PCWCH PCWCHAR PCWSTR -pda pdbs pdbstr +pdcs PDPs pdtobj pdw @@ -1341,6 +1324,7 @@ PIDLIST pids pii piml +pimpl pinvoke pipename pipestr @@ -1372,7 +1356,6 @@ POSXSCROLL POSYSCROLL PPEB ppf -ppguid ppidl PPROC ppropvar @@ -1402,7 +1385,7 @@ PROCESSINFOCLASS PRODEXT PROPERTYID PROPERTYKEY -PROPERTYVAL +propertyval propsheet PROPSHEETHEADER PROPSHEETPAGE @@ -1434,6 +1417,7 @@ ptch ptsz PTYIn PUCHAR +pvar pwch PWDDMCONSOLECONTEXT pws @@ -1519,6 +1503,7 @@ rgn rgp rgpwsz rgrc +rguid rgw RIGHTALIGN RIGHTBUTTON @@ -1526,6 +1511,7 @@ riid RIS roadmap robomac +rodata rosetta RRF rrr @@ -1648,7 +1634,6 @@ sidebyside SIF SIGDN Signtool -SINGLEFLAG SINGLETHREADED siup sixel @@ -1721,6 +1706,7 @@ SYMED SYNCPAINT syscalls SYSCHAR +SYSCOLOR SYSCOMMAND SYSDEADCHAR SYSKEYDOWN @@ -1786,7 +1772,6 @@ TEXTMETRIC TEXTMETRICW textmode texttests -TFCAT TFunction THUMBPOSITION THUMBTRACK @@ -1817,7 +1802,6 @@ Tpqrst tracelogging traceviewpp trackbar -TRACKCOMPOSITION trackpad transitioning Trd @@ -1841,8 +1825,6 @@ TTM TTo tvpp tvtseq -Txtev -typechecked TYUI UAC uap @@ -1860,10 +1842,8 @@ UIACCESS uiacore uiautomationcore uielem -UIELEMENTENABLEDONLY UINTs ul -ulcch uld uldash uldb @@ -1957,7 +1937,6 @@ vstest VSTS VSTT vswhere -vtapi vtapp VTE VTID @@ -2020,7 +1999,6 @@ WINDOWALPHA windowdpiapi WINDOWEDGE windowext -windowime WINDOWINFO windowio windowmetrics @@ -2071,7 +2049,6 @@ WNDCLASSW Wndproc WNegative WNull -wnwb workarea WOutside WOWARM @@ -2110,7 +2087,6 @@ WTs WTSOFTFONT wtw wtypes -Wubi WUX WVerify WWith @@ -2137,9 +2113,7 @@ xes XFG XFile XFORM -xin -xinchaof -xinxinchaof +XIn XManifest XMath xorg diff --git a/OpenConsole.sln b/OpenConsole.sln index 809dd25a34..411f5ca0ed 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -171,6 +171,7 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.Control.Lib", "src\cascadia\TerminalControl\TerminalControlLib.vcxproj", "{CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}" ProjectSection(ProjectDependencies) = postProject {1CF55140-EF6A-4736-A403-957E4F7430BB} = {1CF55140-EF6A-4736-A403-957E4F7430BB} + {2FD12FBB-1DDB-46D8-B818-1023C624CACA} = {2FD12FBB-1DDB-46D8-B818-1023C624CACA} {48D21369-3D7B-4431-9967-24E81292CF63} = {48D21369-3D7B-4431-9967-24E81292CF63} {8222900C-8B6C-452A-91AC-BE95DB04B95F} = {8222900C-8B6C-452A-91AC-BE95DB04B95F} {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} = {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} @@ -412,7 +413,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TerminalStress", "src\tools EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RenderingTests", "src\tools\RenderingTests\RenderingTests.vcxproj", "{37C995E0-2349-4154-8E77-4A52C0C7F46D}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UIHelpers", "src\cascadia\UIHelpers\UIHelpers.vcxproj", "{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "src\cascadia\UIHelpers\UIHelpers.vcxproj", "{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "benchcat", "src\tools\benchcat\benchcat.vcxproj", "{2C836962-9543-4CE5-B834-D28E1F124B66}" EndProject @@ -477,6 +478,7 @@ Global {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.Build.0 = Debug|ARM64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.ActiveCfg = Debug|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.Build.0 = Debug|x64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.Deploy.0 = Debug|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.ActiveCfg = Debug|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.Build.0 = Debug|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 @@ -2443,7 +2445,7 @@ Global {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} {4C8E6BB0-4713-4ADB-BD04-81628ECEAF20} = {81C352DB-1818-45B7-A284-18E259F1CC87} {D57841D1-8294-4F2B-BB8B-D2A35738DECD} = {81C352DB-1818-45B7-A284-18E259F1CC87} - {2FD12FBB-1DDB-46D8-B818-1023C624CACA} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {2FD12FBB-1DDB-46D8-B818-1023C624CACA} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} {3AE13314-1939-4DFA-9C14-38CA0834050C} = {F1995847-4AE5-479A-BBAF-382E51A63532} {DCF55140-EF6A-4736-A403-957E4F7430BB} = {F1995847-4AE5-479A-BBAF-382E51A63532} {1CF55140-EF6A-4736-A403-957E4F7430BB} = {F1995847-4AE5-479A-BBAF-382E51A63532} diff --git a/doc/ORGANIZATION.md b/doc/ORGANIZATION.md index 5f93639d09..18edbdabe5 100644 --- a/doc/ORGANIZATION.md +++ b/doc/ORGANIZATION.md @@ -65,9 +65,6 @@ * `clipboard.cpp` * Handles the command prompt line as you see in CMD.exe (known as the processed input line… most other shells handle this themselves with raw input and don’t use ours. This is a legacy of bad architectural design, putting stuff in conhost not in CMD) * `cmdline.cpp` -* Handles shunting IME data back and forth to the TSF library and to and from the various buffers - * `Conimeinfo.cpp` - * `Convarea.cpp` * Contains the global state for the entire console application * `consoleInformation.cpp` * Stuff related to the low-level server communication over our protocol with the driver diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 4793b86dac..aaf2dcab20 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -914,7 +914,7 @@ const til::small_rle& ROW::Attributes() const noexce TextAttribute ROW::GetAttrByColumn(const til::CoordType column) const { - return _attr.at(_clampedUint16(column)); + return _attr.at(_clampedColumn(column)); } std::vector ROW::GetHyperlinks() const @@ -1097,12 +1097,6 @@ DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_v } } -template -constexpr uint16_t ROW::_clampedUint16(T v) noexcept -{ - return static_cast(clamp(v, 0, 65535)); -} - template constexpr uint16_t ROW::_clampedColumn(T v) const noexcept { diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index 317c935edc..a13e6e7996 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -230,8 +230,6 @@ private: static constexpr uint16_t CharOffsetsTrailer = 0x8000; static constexpr uint16_t CharOffsetsMask = 0x7fff; - template - static constexpr uint16_t _clampedUint16(T v) noexcept; template constexpr uint16_t _clampedColumn(T v) const noexcept; template diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index c7b68926ef..f250749c57 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -49,8 +49,6 @@ filling in the last row, and updating the screen. #pragma once -#include - #include "cursor.h" #include "Row.hpp" #include "TextAttribute.hpp" diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 1646140062..5531ca25cb 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -111,9 +111,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnScrollPositionChanged = std::bind(&ControlCore::_terminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged); - auto pfnTerminalCursorPositionChanged = std::bind(&ControlCore::_terminalCursorPositionChanged, this); - _terminal->SetCursorPositionChangedCallback(pfnTerminalCursorPositionChanged); - auto pfnTerminalTaskbarProgressChanged = std::bind(&ControlCore::_terminalTaskbarProgressChanged, this); _terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged); @@ -171,10 +168,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // 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 @@ -184,16 +177,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // We can throttle this to once every 8ms, which will get us out of // the way of the main output & rendering threads. const auto shared = _shared.lock(); - shared->tsfTryRedrawCanvas = std::make_shared>( - _dispatcher, - TsfRedrawInterval, - [weakThis = get_weak()]() { - if (auto core{ weakThis.get() }; !core->_IsClosing()) - { - core->CursorPositionChanged.raise(*core, nullptr); - } - }); - // NOTE: Calling UpdatePatternLocations from a background // thread is a workaround for us to hit GH#12607 less often. shared->updatePatternLocations = std::make_unique>( @@ -235,7 +218,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // thread. These will be recreated in _setupDispatcherAndCallbacks, when // we're re-attached to a new control (on a possibly new UI thread). const auto shared = _shared.lock(); - shared->tsfTryRedrawCanvas.reset(); shared->updatePatternLocations.reset(); shared->updateScrollBar.reset(); } @@ -456,7 +438,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - wstr: the string of characters to write to the terminal connection. // Return Value: // - - void ControlCore::SendInput(const winrt::hstring& wstr) + void ControlCore::SendInput(const std::wstring_view wstr) { _sendInputToConnection(wstr); } @@ -1040,7 +1022,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto fontWeight = _settings->FontWeight(); _desiredFont = { fontFace, 0, fontWeight.Weight, newSize, CP_UTF8 }; _actualFont = { fontFace, 0, fontWeight.Weight, _desiredFont.GetEngineSize(), CP_UTF8, false }; - _actualFontFaceName = { fontFace }; _desiredFont.SetEnableBuiltinGlyphs(_builtinGlyphs); _desiredFont.SetEnableColorGlyphs(_colorGlyphs); @@ -1445,13 +1426,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation ::base::saturated_cast(fontSize.height) }; } - winrt::hstring ControlCore::FontFaceName() const noexcept - { - // This getter used to return _actualFont.GetFaceName(), however GetFaceName() returns a STL - // string and we need to return a WinRT string. This would require an additional allocation. - // This method is called 10/s by TSFInputControl at the time of writing. - return _actualFontFaceName; - } uint16_t ControlCore::FontWeight() const noexcept { @@ -1622,17 +1596,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - void ControlCore::_terminalCursorPositionChanged() - { - // When the buffer's cursor moves, start the throttled func to - // eventually dispatch a CursorPositionChanged event. - const auto shared = _shared.lock_shared(); - if (shared->tsfTryRedrawCanvas) - { - shared->tsfTryRedrawCanvas->Run(); - } - } - void ControlCore::_terminalTaskbarProgressChanged() { TaskbarProgressChanged.raise(*this, nullptr); @@ -2183,6 +2146,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } + ::Microsoft::Console::Render::Renderer* ControlCore::GetRenderer() const noexcept + { + return _renderer.get(); + } + uint64_t ControlCore::SwapChainHandle() const { // This is only ever called by TermControl::AttachContent, which occurs diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 31d8fe00c1..8748a52ee5 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -100,6 +100,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Microsoft::Terminal::Core::Scheme ColorScheme() const noexcept; void ColorScheme(const winrt::Microsoft::Terminal::Core::Scheme& scheme); + ::Microsoft::Console::Render::Renderer* GetRenderer() const noexcept; uint64_t SwapChainHandle() const; void AttachToNewControl(const Microsoft::Terminal::Control::IKeyBindings& keyBindings); @@ -113,13 +114,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Windows::Foundation::Size FontSizeInDips() const; winrt::Windows::Foundation::Size FontSize() const noexcept; - winrt::hstring FontFaceName() const noexcept; uint16_t FontWeight() const noexcept; til::color ForegroundColor() const; til::color BackgroundColor() const; - void SendInput(const winrt::hstring& wstr); + void SendInput(std::wstring_view wstr); void PasteText(const winrt::hstring& hstr); bool CopySelectionToClipboard(bool singleLine, const Windows::Foundation::IReference& formats); void SelectAll(); @@ -243,8 +243,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring ReadEntireBuffer() const; Control::CommandHistoryContext CommandHistory() const; - static bool IsVintageOpacityAvailable() noexcept; - void AdjustOpacity(const double opacity, const bool relative); void WindowVisibilityChanged(const bool showOrHide); @@ -273,7 +271,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event<> TabColorChanged; til::typed_event<> BackgroundColorChanged; til::typed_event ScrollPositionChanged; - til::typed_event<> CursorPositionChanged; til::typed_event<> TaskbarProgressChanged; til::typed_event<> ConnectionStateChanged; til::typed_event<> HoveredHyperlinkChanged; @@ -298,7 +295,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation private: struct SharedState { - std::shared_ptr> tsfTryRedrawCanvas; std::unique_ptr> updatePatternLocations; std::shared_ptr> updateScrollBar; }; @@ -329,7 +325,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation FontInfoDesired _desiredFont; FontInfo _actualFont; - winrt::hstring _actualFontFaceName; bool _builtinGlyphs = true; bool _colorGlyphs = true; CSSLengthPercentage _cellWidth; @@ -379,7 +374,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _terminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize); - void _terminalCursorPositionChanged(); void _terminalTaskbarProgressChanged(); void _terminalShowWindowChanged(bool showOrHide); void _terminalTextLayoutUpdated(); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 91b58a41c7..d3f7e91962 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -87,7 +87,6 @@ namespace Microsoft.Terminal.Control UInt64 SwapChainHandle { get; }; Windows.Foundation.Size FontSize { get; }; - String FontFaceName { get; }; UInt16 FontWeight { get; }; Double Opacity { get; }; Boolean UseAcrylic { get; }; @@ -172,7 +171,6 @@ namespace Microsoft.Terminal.Control // These events are always called from the UI thread (bugs aside) event Windows.Foundation.TypedEventHandler FontSizeChanged; event Windows.Foundation.TypedEventHandler ScrollPositionChanged; - event Windows.Foundation.TypedEventHandler CursorPositionChanged; event Windows.Foundation.TypedEventHandler ConnectionStateChanged; event Windows.Foundation.TypedEventHandler HoveredHyperlinkChanged; event Windows.Foundation.TypedEventHandler SwapChainChanged; diff --git a/src/cascadia/TerminalControl/TSFInputControl.cpp b/src/cascadia/TerminalControl/TSFInputControl.cpp deleted file mode 100644 index 29ac69e496..0000000000 --- a/src/cascadia/TerminalControl/TSFInputControl.cpp +++ /dev/null @@ -1,440 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "TSFInputControl.h" -#include "TSFInputControl.g.cpp" - -using namespace winrt::Windows::Foundation; -using namespace winrt::Windows::Graphics::Display; -using namespace winrt::Windows::UI::Core; -using namespace winrt::Windows::UI::Text; -using namespace winrt::Windows::UI::Text::Core; -using namespace winrt::Windows::UI::Xaml; - -namespace winrt::Microsoft::Terminal::Control::implementation -{ - TSFInputControl::TSFInputControl() - { - InitializeComponent(); - - // Create a CoreTextEditingContext for since we are acting like a custom edit control - auto manager = CoreTextServicesManager::GetForCurrentView(); - _editContext = manager.CreateEditContext(); - - // InputPane is manually shown inside of TermControl. - _editContext.InputPaneDisplayPolicy(CoreTextInputPaneDisplayPolicy::Manual); - - // Set the input scope to AlphanumericHalfWidth in order to facilitate those CJK input methods to open in English mode by default. - // AlphanumericHalfWidth scope doesn't prevent input method from switching to composition mode, it accepts any character too. - // Besides, Text scope turns on typing intelligence, but that doesn't work in this project. - _editContext.InputScope(CoreTextInputScope::AlphanumericHalfWidth); - - _textRequestedRevoker = _editContext.TextRequested(winrt::auto_revoke, { this, &TSFInputControl::_textRequestedHandler }); - - _selectionRequestedRevoker = _editContext.SelectionRequested(winrt::auto_revoke, { this, &TSFInputControl::_selectionRequestedHandler }); - - _focusRemovedRevoker = _editContext.FocusRemoved(winrt::auto_revoke, { this, &TSFInputControl::_focusRemovedHandler }); - - _textUpdatingRevoker = _editContext.TextUpdating(winrt::auto_revoke, { this, &TSFInputControl::_textUpdatingHandler }); - - _selectionUpdatingRevoker = _editContext.SelectionUpdating(winrt::auto_revoke, { this, &TSFInputControl::_selectionUpdatingHandler }); - - _formatUpdatingRevoker = _editContext.FormatUpdating(winrt::auto_revoke, { this, &TSFInputControl::_formatUpdatingHandler }); - - _layoutRequestedRevoker = _editContext.LayoutRequested(winrt::auto_revoke, { this, &TSFInputControl::_layoutRequestedHandler }); - - _compositionStartedRevoker = _editContext.CompositionStarted(winrt::auto_revoke, { this, &TSFInputControl::_compositionStartedHandler }); - - _compositionCompletedRevoker = _editContext.CompositionCompleted(winrt::auto_revoke, { this, &TSFInputControl::_compositionCompletedHandler }); - } - - // Method Description: - // - Prepares this TSFInputControl to be removed from the UI hierarchy. - void TSFInputControl::Close() - { - // Explicitly disconnect the LayoutRequested handler -- it can cause problems during application teardown. - // See GH#4159 for more info. - // Also disconnect compositionCompleted and textUpdating explicitly. It seems to occasionally cause problems if - // a composition is active during application teardown. - _layoutRequestedRevoker.revoke(); - _compositionCompletedRevoker.revoke(); - _textUpdatingRevoker.revoke(); - } - - // Method Description: - // - NotifyFocusEnter handler for notifying CoreEditTextContext of focus enter - // when TerminalControl receives focus. - // Arguments: - // - - // Return Value: - // - - void TSFInputControl::NotifyFocusEnter() - { - _editContext.NotifyFocusEnter(); - _focused = true; - } - - // Method Description: - // - NotifyFocusEnter handler for notifying CoreEditTextContext of focus leaving. - // when TerminalControl no longer has focus. - // Arguments: - // - - // Return Value: - // - - void TSFInputControl::NotifyFocusLeave() - { - _editContext.NotifyFocusLeave(); - _focused = false; - } - - // Method Description: - // - Clears the input buffer and tells the text server to clear their buffer as well. - // Also clears the TextBlock and sets the active text starting point to 0. - // Arguments: - // - - // Return Value: - // - - void TSFInputControl::ClearBuffer() - { - if (!_inputBuffer.empty()) - { - _inputBuffer.clear(); - _selection = {}; - _activeTextStart = 0; - _editContext.NotifyTextChanged({ 0, INT32_MAX }, 0, _selection); - TextBlock().Text({}); - } - } - - // Method Description: - // - Redraw the canvas if certain dimensions have changed since the last - // redraw. This includes the Terminal cursor position, the Canvas width, and the TextBlock height. - // Arguments: - // - - // Return Value: - // - - void TSFInputControl::TryRedrawCanvas() - try - { - if (!_focused || !Canvas()) - { - return; - } - - // Get the cursor position in text buffer position - auto cursorArgs = winrt::make_self(); - CurrentCursorPosition.raise(*this, *cursorArgs); - const til::point cursorPos{ til::math::flooring, cursorArgs->CurrentPosition() }; - - const auto actualCanvasWidth{ Canvas().ActualWidth() }; - const auto actualTextBlockHeight{ TextBlock().ActualHeight() }; - const auto actualWindowBounds{ CoreWindow::GetForCurrentThread().Bounds() }; - - if (_currentTerminalCursorPos == cursorPos && - _currentCanvasWidth == actualCanvasWidth && - _currentTextBlockHeight == actualTextBlockHeight && - _currentWindowBounds == actualWindowBounds) - { - return; - } - - _currentTerminalCursorPos = cursorPos; - _currentCanvasWidth = actualCanvasWidth; - _currentTextBlockHeight = actualTextBlockHeight; - _currentWindowBounds = actualWindowBounds; - - _RedrawCanvas(); - } - CATCH_LOG() - - // Method Description: - // - Redraw the Canvas and update the current Text Bounds and Control Bounds for - // the CoreTextEditContext. - // Arguments: - // - - // Return Value: - // - - void TSFInputControl::_RedrawCanvas() - { - // Get Font Info as we use this is the pixel size for characters in the display - auto fontArgs = winrt::make_self(); - CurrentFontInfo.raise(*this, *fontArgs); - - const til::size fontSize{ til::math::flooring, fontArgs->FontSize() }; - - // Convert text buffer cursor position to client coordinate position - // within the window. This point is in _pixels_ - const auto clientCursorPos{ _currentTerminalCursorPos * fontSize }; - - // Get scale factor for view - const auto scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); - - const til::point clientCursorInDips{ til::math::flooring, clientCursorPos.x / scaleFactor, clientCursorPos.y / scaleFactor }; - - // Position our TextBlock at the cursor position - Canvas().SetLeft(TextBlock(), clientCursorInDips.x); - Canvas().SetTop(TextBlock(), clientCursorInDips.y); - - // calculate FontSize in pixels from Points - const double fontSizePx = (fontSize.height * 72) / USER_DEFAULT_SCREEN_DPI; - const auto unscaledFontSizePx = fontSizePx / scaleFactor; - - // Make sure to unscale the font size to correct for DPI! XAML needs - // things in DIPs, and the fontSize is in pixels. - TextBlock().FontSize(unscaledFontSizePx); - TextBlock().FontFamily(Media::FontFamily(fontArgs->FontFace())); - TextBlock().FontWeight(fontArgs->FontWeight()); - - // TextBlock's actual dimensions right after initialization is 0w x 0h. So, - // if an IME is displayed before TextBlock has text (like showing the emoji picker - // using Win+.), it'll be placed higher than intended. - TextBlock().MinWidth(unscaledFontSizePx); - TextBlock().MinHeight(unscaledFontSizePx); - _currentTextBlockHeight = std::max(unscaledFontSizePx, _currentTextBlockHeight); - - const auto widthToTerminalEnd = _currentCanvasWidth - clientCursorInDips.x; - // Make sure that we're setting the MaxWidth to a positive number - a - // negative number here will crash us in mysterious ways with a useless - // stack trace - const auto newMaxWidth = std::max(0.0, widthToTerminalEnd); - TextBlock().MaxWidth(newMaxWidth); - - // Get window in screen coordinates, this is the entire window including - // tabs. THIS IS IN DIPs - const til::point windowOrigin{ til::math::flooring, _currentWindowBounds.X, _currentWindowBounds.Y }; - - // Get the offset (margin + tabs, etc..) of the control within the window - const til::point controlOrigin{ til::math::flooring, - this->TransformToVisual(nullptr).TransformPoint(Point(0, 0)) }; - - // The controlAbsoluteOrigin is the origin of the control relative to - // the origin of the displays. THIS IS IN DIPs - const auto controlAbsoluteOrigin{ windowOrigin + controlOrigin }; - - // Convert the control origin to pixels - const auto scaledFrameOrigin = til::point{ til::math::flooring, controlAbsoluteOrigin.x * scaleFactor, controlAbsoluteOrigin.y * scaleFactor }; - - // Get the location of the cursor in the display, in pixels. - auto screenCursorPos{ scaledFrameOrigin + clientCursorPos }; - - // GH #5007 - make sure to account for wrapping the IME composition at - // the right side of the viewport. - const auto textBlockHeight = ::base::ClampMul(_currentTextBlockHeight, scaleFactor); - - // Get the bounds of the composition text, in pixels. - const til::rect textBounds{ til::point{ screenCursorPos.x, screenCursorPos.y }, - til::size{ 0, textBlockHeight } }; - - _currentTextBounds = textBounds.to_winrt_rect(); - - _currentControlBounds = Rect(static_cast(screenCursorPos.x), - static_cast(screenCursorPos.y), - 0.0f, - static_cast(fontSize.height)); - } - - // Method Description: - // - Handler for LayoutRequested event by CoreEditContext responsible - // for returning the current position the IME should be placed - // in screen coordinates on the screen. TSFInputControls internal - // XAML controls (TextBlock/Canvas) are also positioned and updated. - // NOTE: documentation says application should handle this event - // Arguments: - // - sender: CoreTextEditContext sending the request. - // - args: CoreTextLayoutRequestedEventArgs to be updated with position information. - // Return Value: - // - - void TSFInputControl::_layoutRequestedHandler(CoreTextEditContext sender, const CoreTextLayoutRequestedEventArgs& args) - { - auto request = args.Request(); - - TryRedrawCanvas(); - - // Set the text block bounds - request.LayoutBounds().TextBounds(_currentTextBounds); - - // Set the control bounds of the whole control - request.LayoutBounds().ControlBounds(_currentControlBounds); - } - - // Method Description: - // - Handler for CompositionStarted event by CoreEditContext responsible - // for making internal TSFInputControl controls visible. - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - args: CoreTextCompositionStartedEventArgs. Not used in method. - // Return Value: - // - - void TSFInputControl::_compositionStartedHandler(CoreTextEditContext sender, const CoreTextCompositionStartedEventArgs& /*args*/) - { - _inComposition = true; - } - - // Method Description: - // - Handler for CompositionCompleted event by CoreEditContext responsible - // for making internal TSFInputControl controls visible. - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - args: CoreTextCompositionCompletedEventArgs. Not used in method. - // Return Value: - // - - void TSFInputControl::_compositionCompletedHandler(CoreTextEditContext sender, const CoreTextCompositionCompletedEventArgs& /*args*/) - { - _inComposition = false; - _SendAndClearText(); - } - - // Method Description: - // - Handler for FocusRemoved event by CoreEditContext responsible - // for removing focus for the TSFInputControl control accordingly - // when focus was forcibly removed from text input control. - // NOTE: Documentation says application should handle this event - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - object: CoreTextCompositionStartedEventArgs. Not used in method. - // Return Value: - // - - void TSFInputControl::_focusRemovedHandler(CoreTextEditContext sender, const winrt::Windows::Foundation::IInspectable& /*object*/) - { - } - - // Method Description: - // - Handler for TextRequested event by CoreEditContext responsible - // for returning the range of text requested. - // NOTE: Documentation says application should handle this event - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - args: CoreTextTextRequestedEventArgs to be updated with requested range text. - // Return Value: - // - - void TSFInputControl::_textRequestedHandler(CoreTextEditContext sender, const CoreTextTextRequestedEventArgs& args) - { - try - { - const auto range = args.Request().Range(); - const auto text = _inputBuffer.substr( - range.StartCaretPosition, - range.EndCaretPosition - range.StartCaretPosition); - args.Request().Text(text); - } - CATCH_LOG(); - } - - // Method Description: - // - Handler for SelectionRequested event by CoreEditContext responsible - // for returning the currently selected text. - // TSFInputControl currently doesn't allow selection, so nothing happens. - // NOTE: Documentation says application should handle this event - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - args: CoreTextSelectionRequestedEventArgs for providing data for the SelectionRequested event. Not used in method. - // Return Value: - // - - void TSFInputControl::_selectionRequestedHandler(CoreTextEditContext sender, const CoreTextSelectionRequestedEventArgs& args) - { - args.Request().Selection(_selection); - } - - // Method Description: - // - Handler for SelectionUpdating event by CoreEditContext responsible - // for handling modifications to the range of text currently selected. - // TSFInputControl doesn't currently allow selection, so nothing happens. - // NOTE: Documentation says application should set its selection range accordingly - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - args: CoreTextSelectionUpdatingEventArgs for providing data for the SelectionUpdating event. Not used in method. - // Return Value: - // - - void TSFInputControl::_selectionUpdatingHandler(CoreTextEditContext sender, const CoreTextSelectionUpdatingEventArgs& args) - { - _selection = args.Selection(); - args.Result(CoreTextSelectionUpdatingResult::Succeeded); - } - - // Method Description: - // - Handler for TextUpdating event by CoreEditContext responsible - // for handling text updates. - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - args: CoreTextTextUpdatingEventArgs contains new text to update buffer with. - // Return Value: - // - - void TSFInputControl::_textUpdatingHandler(CoreTextEditContext sender, const CoreTextTextUpdatingEventArgs& args) - { - try - { - const auto incomingText = args.Text(); - const auto range = args.Range(); - - _inputBuffer = _inputBuffer.replace( - range.StartCaretPosition, - range.EndCaretPosition - range.StartCaretPosition, - incomingText); - _selection = args.NewSelection(); - // GH#5054: Pressing backspace might move the caret before the _activeTextStart. - _activeTextStart = std::min(_activeTextStart, _inputBuffer.size()); - - // Emojis/Kaomojis/Symbols chosen through the IME without starting composition - // will be sent straight through to the terminal. - if (!_inComposition) - { - _SendAndClearText(); - } - else - { - Canvas().Visibility(Visibility::Visible); - const auto text = _inputBuffer.substr(_activeTextStart); - TextBlock().Text(text); - } - - // Notify the TSF that the update succeeded - args.Result(CoreTextTextUpdatingResult::Succeeded); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - - // indicate updating failed. - args.Result(CoreTextTextUpdatingResult::Failed); - } - } - - void TSFInputControl::_SendAndClearText() - { - const auto text = _inputBuffer.substr(_activeTextStart); - if (text.empty()) - { - return; - } - - CompositionCompleted.raise(text); - - _activeTextStart = _inputBuffer.size(); - - TextBlock().Text({}); - - // After we reset the TextBlock to empty string, we want to make sure - // ActualHeight reflects the respective height. It seems that ActualHeight - // isn't updated until there's new text in the TextBlock, so the next time a user - // invokes "Win+." for the emoji picker IME, it would end up - // using the pre-reset TextBlock().ActualHeight(). - TextBlock().UpdateLayout(); - - // hide the controls until text input starts again - Canvas().Visibility(Visibility::Collapsed); - } - - // Method Description: - // - Handler for FormatUpdating event by CoreEditContext responsible - // for handling different format updates for a particular range of text. - // TSFInputControl doesn't do anything with this event. - // Arguments: - // - sender: CoreTextEditContext sending the request. Not used in method. - // - args: CoreTextFormatUpdatingEventArgs Provides data for the FormatUpdating event. Not used in method. - // Return Value: - // - - void TSFInputControl::_formatUpdatingHandler(CoreTextEditContext sender, const CoreTextFormatUpdatingEventArgs& /*args*/) - { - } -} diff --git a/src/cascadia/TerminalControl/TSFInputControl.h b/src/cascadia/TerminalControl/TSFInputControl.h deleted file mode 100644 index 95a79b96aa..0000000000 --- a/src/cascadia/TerminalControl/TSFInputControl.h +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once -#include "TSFInputControl.g.h" -#include "CursorPositionEventArgs.g.h" -#include "FontInfoEventArgs.g.h" - -namespace winrt::Microsoft::Terminal::Control::implementation -{ - struct CursorPositionEventArgs : - public CursorPositionEventArgsT - { - public: - CursorPositionEventArgs() = default; - - WINRT_PROPERTY(Windows::Foundation::Point, CurrentPosition); - }; - - struct FontInfoEventArgs : - public FontInfoEventArgsT - { - public: - FontInfoEventArgs() = default; - - WINRT_PROPERTY(Windows::Foundation::Size, FontSize); - - WINRT_PROPERTY(winrt::hstring, FontFace); - - WINRT_PROPERTY(Windows::UI::Text::FontWeight, FontWeight); - }; - - struct TSFInputControl : TSFInputControlT - { - public: - TSFInputControl(); - - void NotifyFocusEnter(); - void NotifyFocusLeave(); - void ClearBuffer(); - void TryRedrawCanvas(); - - void Close(); - - // -------------------------------- WinRT Events --------------------------------- - til::typed_event CurrentCursorPosition; - til::typed_event CurrentFontInfo; - til::event CompositionCompleted; - - private: - void _layoutRequestedHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextLayoutRequestedEventArgs& args); - void _compositionStartedHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextCompositionStartedEventArgs& args); - void _compositionCompletedHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextCompositionCompletedEventArgs& args); - void _focusRemovedHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::Foundation::IInspectable& object); - void _selectionRequestedHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextSelectionRequestedEventArgs& args); - void _textRequestedHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextTextRequestedEventArgs& args); - void _selectionUpdatingHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextSelectionUpdatingEventArgs& args); - void _textUpdatingHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextTextUpdatingEventArgs& args); - void _formatUpdatingHandler(winrt::Windows::UI::Text::Core::CoreTextEditContext sender, const winrt::Windows::UI::Text::Core::CoreTextFormatUpdatingEventArgs& args); - - void _SendAndClearText(); - void _RedrawCanvas(); - - winrt::Windows::UI::Text::Core::CoreTextEditContext::TextRequested_revoker _textRequestedRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::SelectionRequested_revoker _selectionRequestedRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::FocusRemoved_revoker _focusRemovedRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::TextUpdating_revoker _textUpdatingRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::SelectionUpdating_revoker _selectionUpdatingRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::FormatUpdating_revoker _formatUpdatingRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::LayoutRequested_revoker _layoutRequestedRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::CompositionStarted_revoker _compositionStartedRevoker; - winrt::Windows::UI::Text::Core::CoreTextEditContext::CompositionCompleted_revoker _compositionCompletedRevoker; - - Windows::UI::Text::Core::CoreTextEditContext _editContext{ nullptr }; - std::wstring _inputBuffer; - winrt::Windows::UI::Text::Core::CoreTextRange _selection{}; - size_t _activeTextStart = 0; - bool _inComposition = false; - bool _focused = false; - - til::point _currentTerminalCursorPos{}; - double _currentCanvasWidth = 0.0; - double _currentTextBlockHeight = 0.0; - winrt::Windows::Foundation::Rect _currentControlBounds{}; - winrt::Windows::Foundation::Rect _currentTextBounds{}; - winrt::Windows::Foundation::Rect _currentWindowBounds{}; - }; -} -namespace winrt::Microsoft::Terminal::Control::factory_implementation -{ - BASIC_FACTORY(TSFInputControl); -} diff --git a/src/cascadia/TerminalControl/TSFInputControl.idl b/src/cascadia/TerminalControl/TSFInputControl.idl deleted file mode 100644 index 6a6f6814f2..0000000000 --- a/src/cascadia/TerminalControl/TSFInputControl.idl +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -namespace Microsoft.Terminal.Control -{ - delegate void CompositionCompletedEventArgs(String text); - - runtimeclass CursorPositionEventArgs - { - Windows.Foundation.Point CurrentPosition { get; set; }; - } - - runtimeclass FontInfoEventArgs - { - String FontFace { get; set; }; - Windows.Foundation.Size FontSize { get; set; }; - Windows.UI.Text.FontWeight FontWeight { get; set; }; - } - - [default_interface] - runtimeclass TSFInputControl : Windows.UI.Xaml.Controls.UserControl - { - TSFInputControl(); - - event CompositionCompletedEventArgs CompositionCompleted; - event Windows.Foundation.TypedEventHandler CurrentCursorPosition; - event Windows.Foundation.TypedEventHandler CurrentFontInfo; - - void NotifyFocusEnter(); - void NotifyFocusLeave(); - void ClearBuffer(); - void TryRedrawCanvas(); - - void Close(); - } -} diff --git a/src/cascadia/TerminalControl/TSFInputControl.xaml b/src/cascadia/TerminalControl/TSFInputControl.xaml deleted file mode 100644 index bc7fca22bf..0000000000 --- a/src/cascadia/TerminalControl/TSFInputControl.xaml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 96c78f0afc..acab829312 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -5,10 +5,10 @@ #include "TermControl.h" #include -#include #include "TermControlAutomationPeer.h" #include "../../renderer/atlas/AtlasEngine.h" +#include "../../tsf/Handle.h" #include "TermControl.g.cpp" @@ -41,11 +41,130 @@ constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds( constexpr const auto TerminalWarningBellInterval = std::chrono::milliseconds(1000); DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::CopyFormat); - DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::MouseButtonState); +static Microsoft::Console::TSF::Handle& GetTSFHandle() +{ + // https://en.cppreference.com/w/cpp/language/storage_duration + // > Variables declared at block scope with the specifier static or thread_local + // > [...] are initialized the first time control passes through their declaration + // --> Lazy, per-(window-)thread initialization of the TSF handle + thread_local auto s_tsf = ::Microsoft::Console::TSF::Handle::Create(); + return s_tsf; +} + namespace winrt::Microsoft::Terminal::Control::implementation { + TsfDataProvider::TsfDataProvider(TermControl* termControl) noexcept : + _termControl{ termControl } + { + } + + STDMETHODIMP TsfDataProvider::QueryInterface(REFIID, void**) noexcept + { + return E_NOTIMPL; + } + + ULONG STDMETHODCALLTYPE TsfDataProvider::AddRef() noexcept + { + return 1; + } + + ULONG STDMETHODCALLTYPE TsfDataProvider::Release() noexcept + { + return 1; + } + + HWND TsfDataProvider::GetHwnd() + { + if (!_hwnd) + { + // WinUI's WinRT based TSF runs in its own window "Windows.UI.Input.InputSite.WindowClass" (..."great") + // and in order for us to snatch the focus away from that one we need to find its HWND. + // The way we do it here is by finding the existing, active TSF context and getting the HWND from it. + _hwnd = GetTSFHandle().FindWindowOfActiveTSF(); + if (!_hwnd) + { + _hwnd = reinterpret_cast(_termControl->OwningHwnd()); + } + } + return _hwnd; + } + + RECT TsfDataProvider::GetViewport() + { + const auto scaleFactor = static_cast(DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel()); + const auto globalOrigin = CoreWindow::GetForCurrentThread().Bounds(); + const auto localOrigin = _termControl->TransformToVisual(nullptr).TransformPoint({}); + const auto size = _termControl->ActualSize(); + + const auto left = globalOrigin.X + localOrigin.X; + const auto top = globalOrigin.Y + localOrigin.Y; + const auto right = left + size.x; + const auto bottom = top + size.y; + + return { + lroundf(left * scaleFactor), + lroundf(top * scaleFactor), + lroundf(right * scaleFactor), + lroundf(bottom * scaleFactor), + }; + } + + RECT TsfDataProvider::GetCursorPosition() + { + const auto core = _getCore(); + if (!core) + { + return {}; + } + + const auto scaleFactor = static_cast(DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel()); + const auto globalOrigin = CoreWindow::GetForCurrentThread().Bounds(); + const auto localOrigin = _termControl->TransformToVisual(nullptr).TransformPoint({}); + const auto padding = _termControl->GetPadding(); + const auto cursorPosition = core->CursorPosition(); + const auto fontSize = core->FontSize(); + + // fontSize is not in DIPs, so we need to first multiply by scaleFactor and then do the rest. + const auto left = (globalOrigin.X + localOrigin.X + static_cast(padding.Left)) * scaleFactor + cursorPosition.X * fontSize.Width; + const auto top = (globalOrigin.Y + localOrigin.Y + static_cast(padding.Top)) * scaleFactor + cursorPosition.Y * fontSize.Height; + const auto right = left + fontSize.Width; + const auto bottom = top + fontSize.Height; + + return { + lroundf(left), + lroundf(top), + lroundf(right), + lroundf(bottom), + }; + } + + void TsfDataProvider::HandleOutput(std::wstring_view text) + { + const auto core = _getCore(); + if (!core) + { + return; + } + core->SendInput(text); + } + + ::Microsoft::Console::Render::Renderer* TsfDataProvider::GetRenderer() + { + const auto core = _getCore(); + if (!core) + { + return nullptr; + } + return core->GetRenderer(); + } + + ControlCore* TsfDataProvider::_getCore() const noexcept + { + return get_self(_termControl->_core); + } + TermControl::TermControl(IControlSettings settings, Control::IControlAppearance unfocusedAppearance, TerminalConnection::ITerminalConnection connection) : @@ -154,7 +273,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // attached content before we set up the throttled func, and that'll A/V _revokers.coreScrollPositionChanged = _core.ScrollPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_ScrollPositionChanged }); _revokers.WarningBell = _core.WarningBell(winrt::auto_revoke, { get_weak(), &TermControl::_coreWarningBell }); - _revokers.CursorPositionChanged = _core.CursorPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_CursorPositionChanged }); static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast(1.0 / 30.0 * 1000000)); _autoScrollTimer.Interval(AutoScrollUpdateInterval); @@ -590,18 +708,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation SelectionStartMarker().Fill(cursorColorBrush); SelectionEndMarker().Fill(cursorColorBrush); - // Set TSF Foreground - Media::SolidColorBrush foregroundBrush{}; - if (_core.Settings().UseBackgroundImageForWindow()) - { - foregroundBrush.Color(Windows::UI::Colors::Transparent()); - } - else - { - foregroundBrush.Color(static_cast(newAppearance.DefaultForeground())); - } - TSFInputControl().Foreground(foregroundBrush); - _core.ApplyAppearance(_focused); } @@ -654,8 +760,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto newMargin = ParseThicknessFromPadding(settings.Padding()); SwapChainPanel().Margin(newMargin); - TSFInputControl().Margin(newMargin); - // Apply settings for scrollbar if (settings.ScrollState() == ScrollbarState::Hidden) { @@ -1170,19 +1274,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - // GH#11479: TSF wants to be notified of any character input via ICoreTextEditContext::NotifyTextChanged(). - // TSF is built and tested around the idea that you inform it of any text changes that happen - // when it doesn't currently compose characters. For instance writing "xin chaof" with the - // Vietnamese IME should produce "xin chào". After writing "xin" it'll emit a composition - // completion event and we'll write "xin" to the shell. It now has no input focus and won't know - // about the whitespace. If you then write "chaof", it'll emit another composition completion - // event for "xinchaof" and the resulting output in the shell will finally read "xinxinchaof". - // A composition completion event technically doesn't mean that the completed text is now - // immutable after all. We could (and probably should) inform TSF of any input changes, - // but we technically aren't a text input field. The immediate solution was - // to simply force TSF to clear its text whenever we have input focus. - TSFInputControl().ClearBuffer(); - HidePointerCursor.raise(*this, nullptr); const auto ch = e.Character(); @@ -1970,12 +2061,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation { return; } - - if (TSFInputControl() != nullptr) - { - TSFInputControl().NotifyFocusEnter(); - } - if (_cursorTimer) { // When the terminal focuses, show the cursor immediately @@ -1996,6 +2081,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { UpdateAppearance(_core.FocusedAppearance()); } + + GetTSFHandle().Focus(&_tsfDataProvider); } // Method Description: @@ -2021,11 +2108,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _interactivity.LostFocus(); } - if (TSFInputControl() != nullptr) - { - TSFInputControl().NotifyFocusLeave(); - } - if (_cursorTimer && !_displayCursorWhileBlurred()) { _cursorTimer.Stop(); @@ -2043,6 +2125,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { UpdateAppearance(_core.UnfocusedAppearance()); } + + GetTSFHandle().Unfocus(&_tsfDataProvider); } // Method Description: @@ -2169,27 +2253,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - // Method Description: - // - Tells TSFInputControl to redraw the Canvas/TextBlock so it'll update - // to be where the current cursor position is. - // Arguments: - // - N/A - winrt::fire_and_forget TermControl::_CursorPositionChanged(const IInspectable& /*sender*/, - const IInspectable& /*args*/) - { - // 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 wil::resume_foreground(Dispatcher()); - if (auto control{ weakThis.get() }; !control->_IsClosing()) - { - control->TSFInputControl().TryRedrawCanvas(); - } - } - hstring TermControl::Title() { return _core.Title(); @@ -2304,9 +2367,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _revokers = {}; - // Disconnect the TSF input control so it doesn't receive EditContext events. - TSFInputControl().Close(); - // At the time of writing, closing the last tab of a window inexplicably // does not lead to the destruction of the remaining TermControl instance(s). // On Win10 we don't destroy window threads due to bugs in DesktopWindowXamlSource. @@ -2317,6 +2377,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation _cursorTimer.Stop(); _blinkTimer.Stop(); + // This is absolutely crucial, as the TSF code tries to hold a strong reference to _tsfDataProvider, + // but right now _tsfDataProvider implements IUnknown as a no-op. This ensures that TSF stops referencing us. + // ~TermControl() calls Close() so this should be safe. + GetTSFHandle().Unfocus(&_tsfDataProvider); + if (!_detached) { _interactivity.Close(); @@ -2742,62 +2807,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation return relativeToMarginInPixels; } - // Method Description: - // - Composition Completion handler for the TSFInputControl that - // handles writing text out to TerminalConnection - // Arguments: - // - text: the text to write to TerminalConnection - // Return Value: - // - - void TermControl::_CompositionCompleted(winrt::hstring text) - { - if (_IsClosing()) - { - return; - } - - // SendInput will take care of broadcasting for us. - SendInput(text); - } - - // Method Description: - // - CurrentCursorPosition handler for the TSFInputControl that - // handles returning current cursor position. - // Arguments: - // - eventArgs: event for storing the current cursor position - // Return Value: - // - - void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, - const CursorPositionEventArgs& eventArgs) - { - if (!_initializedTerminal) - { - // fake it - eventArgs.CurrentPosition({ 0, 0 }); - return; - } - - const auto cursorPos = _core.CursorPosition(); - eventArgs.CurrentPosition({ static_cast(cursorPos.X), static_cast(cursorPos.Y) }); - } - - // Method Description: - // - FontInfo handler for the TSFInputControl that - // handles returning current font information - // Arguments: - // - eventArgs: event for storing the current font information - // Return Value: - // - - void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, - const FontInfoEventArgs& eventArgs) - { - eventArgs.FontSize(CharacterDimensions()); - eventArgs.FontFace(_core.FontFaceName()); - ::winrt::Windows::UI::Text::FontWeight weight; - weight.Weight = _core.FontWeight(); - eventArgs.FontWeight(weight); - } - // Method Description: // - Calculates speed of single axis of auto scrolling. It has to allow for both // fast and precise selection. diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 4240bb23ef..4da7ec63d0 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -3,14 +3,12 @@ #pragma once -#include "TermControl.g.h" -#include "XamlLights.h" -#include "EventArgs.h" -#include "../../renderer/base/Renderer.hpp" -#include "../../renderer/uia/UiaRenderer.hpp" -#include "../../cascadia/TerminalCore/Terminal.hpp" -#include "../buffer/out/search.h" #include "SearchBoxControl.h" +#include "TermControl.g.h" +#include "../../buffer/out/search.h" +#include "../../cascadia/TerminalCore/Terminal.hpp" +#include "../../renderer/uia/UiaRenderer.hpp" +#include "../../tsf/Handle.h" #include "ControlInteractivity.h" #include "ControlSettings.h" @@ -22,6 +20,30 @@ namespace Microsoft::Console::VirtualTerminal namespace winrt::Microsoft::Terminal::Control::implementation { + struct TermControl; + + struct TsfDataProvider : ::Microsoft::Console::TSF::IDataProvider + { + explicit TsfDataProvider(TermControl* termControl) noexcept; + virtual ~TsfDataProvider() = default; + + STDMETHODIMP QueryInterface(REFIID riid, void** ppvObj) noexcept override; + ULONG STDMETHODCALLTYPE AddRef() noexcept override; + ULONG STDMETHODCALLTYPE Release() noexcept override; + + HWND GetHwnd() override; + RECT GetViewport() override; + RECT GetCursorPosition() override; + void HandleOutput(std::wstring_view text) override; + ::Microsoft::Console::Render::Renderer* GetRenderer() override; + + private: + ControlCore* _getCore() const noexcept; + + TermControl* _termControl = nullptr; + HWND _hwnd = nullptr; + }; + struct TermControl : TermControlT { TermControl(Control::ControlInteractivity content); @@ -201,6 +223,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation private: friend struct TermControlT; // friend our parent so it can bind private event handlers + friend struct TsfDataProvider; // NOTE: _uiaEngine must be ordered before _core. // @@ -213,7 +236,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::TermControlAutomationPeer _automationPeer{ nullptr }; Control::ControlInteractivity _interactivity{ nullptr }; Control::ControlCore _core{ nullptr }; - + TsfDataProvider _tsfDataProvider{ this }; winrt::com_ptr _searchBox; bool _closing{ false }; @@ -328,7 +351,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _TerminalTabColorChanged(const std::optional color); void _ScrollPositionChanged(const IInspectable& sender, const Control::ScrollPositionChangedArgs& args); - winrt::fire_and_forget _CursorPositionChanged(const IInspectable& sender, const IInspectable& args); bool _CapturePointer(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e); bool _ReleasePointerCapture(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e); @@ -354,11 +376,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); void _UpdateSearchScrollMarks(); - // TSFInputControl Handlers - void _CompositionCompleted(winrt::hstring text); - void _CurrentCursorPositionHandler(const IInspectable& sender, const CursorPositionEventArgs& eventArgs); - void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs); - void _hoveredHyperlinkChanged(const IInspectable& sender, const IInspectable& args); winrt::fire_and_forget _updateSelectionMarkers(IInspectable sender, Control::UpdateSelectionMarkersEventArgs args); @@ -388,7 +405,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation { Control::ControlCore::ScrollPositionChanged_revoker coreScrollPositionChanged; Control::ControlCore::WarningBell_revoker WarningBell; - Control::ControlCore::CursorPositionChanged_revoker CursorPositionChanged; Control::ControlCore::RendererEnteredErrorState_revoker RendererEnteredErrorState; Control::ControlCore::BackgroundColorChanged_revoker BackgroundColorChanged; Control::ControlCore::FontSizeChanged_revoker FontSizeChanged; diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index 043a95d3b2..feb8508440 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -1332,11 +1332,6 @@ - - InteractivityAutomationPeer.idl - - TSFInputControl.xaml - @@ -102,9 +99,6 @@ TermControl.xaml - - TSFInputControl.xaml - TermControlAutomationPeer.idl @@ -137,9 +131,6 @@ - - TSFInputControl.xaml - @@ -149,9 +140,6 @@ Designer - - Designer - @@ -160,15 +148,16 @@ - - - - - - - - + + + + + + + + + false false diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 4eccd3be99..25ea33fd47 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -422,19 +422,7 @@ CATCH_RETURN() void Terminal::Write(std::wstring_view stringView) { - const auto& cursor = _activeBuffer().GetCursor(); - const til::point cursorPosBefore{ cursor.GetPosition() }; - _stateMachine->ProcessString(stringView); - - const til::point cursorPosAfter{ cursor.GetPosition() }; - - // Firing the CursorPositionChanged event is very expensive so we try not to - // do that when the cursor does not need to be redrawn. - if (cursorPosBefore != cursorPosAfter) - { - _NotifyTerminalCursorPositionChanged(); - } } // Method Description: @@ -1099,18 +1087,6 @@ void Terminal::_NotifyScrollEvent() } } -void Terminal::_NotifyTerminalCursorPositionChanged() noexcept -{ - if (_pfnCursorPositionChanged) - { - try - { - _pfnCursorPositionChanged(); - } - CATCH_LOG(); - } -} - void Terminal::SetWriteInputCallback(std::function pfn) noexcept { _pfnWriteInput.swap(pfn); @@ -1136,11 +1112,6 @@ void Terminal::SetScrollPositionChangedCallback(std::function pfn) noexcept -{ - _pfnCursorPositionChanged.swap(pfn); -} - // Method Description: // - Allows settings a callback for settings the taskbar progress indicator // Arguments: diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 6a6017db88..7f1c20789e 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -189,7 +189,7 @@ public: #pragma region IRenderData Microsoft::Console::Types::Viewport GetViewport() noexcept override; til::point GetTextBufferEndPosition() const noexcept override; - const TextBuffer& GetTextBuffer() const noexcept override; + TextBuffer& GetTextBuffer() const noexcept override; const FontInfo& GetFontInfo() const noexcept override; void LockConsole() noexcept override; @@ -203,7 +203,6 @@ public: ULONG GetCursorPixelWidth() const noexcept override; CursorType GetCursorStyle() const noexcept override; bool IsCursorDoubleWidth() const override; - const std::vector GetOverlays() const noexcept override; const bool IsGridLineDrawingAllowed() noexcept override; const std::wstring GetHyperlinkUri(uint16_t id) const override; const std::wstring GetHyperlinkCustomId(uint16_t id) const override; @@ -228,7 +227,6 @@ public: void SetTitleChangedCallback(std::function pfn) noexcept; void SetCopyToClipboardCallback(std::function pfn) noexcept; void SetScrollPositionChangedCallback(std::function pfn) noexcept; - void SetCursorPositionChangedCallback(std::function pfn) noexcept; void TaskbarProgressChangedCallback(std::function pfn) noexcept; void SetShowWindowCallback(std::function pfn) noexcept; void SetPlayMidiNoteCallback(std::function pfn) noexcept; @@ -339,7 +337,6 @@ private: til::recursive_ticket_lock _readWriteLock; std::function _pfnScrollPositionChanged; - std::function _pfnCursorPositionChanged; std::function _pfnTaskbarProgressChanged; std::function _pfnShowWindowChanged; std::function _pfnPlayMidiNote; @@ -457,9 +454,6 @@ private: til::CoordType _ScrollToPoints(const til::point coordStart, const til::point coordEnd); void _NotifyScrollEvent(); - - void _NotifyTerminalCursorPositionChanged() noexcept; - bool _inAltBuffer() const noexcept; TextBuffer& _activeBuffer() const noexcept; void _updateUrlDetection(); diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index a9eaf11fa8..ec28b59d56 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -22,7 +22,7 @@ til::point Terminal::GetTextBufferEndPosition() const noexcept return { _GetMutableViewport().Width() - 1, ViewEndIndex() }; } -const TextBuffer& Terminal::GetTextBuffer() const noexcept +TextBuffer& Terminal::GetTextBuffer() const noexcept { return _activeBuffer(); } @@ -79,11 +79,6 @@ bool Terminal::IsCursorDoubleWidth() const return buffer.GetRowByOffset(position.y).DbcsAttrAt(position.x) != DbcsAttribute::Single; } -const std::vector Terminal::GetOverlays() const noexcept -{ - return {}; -} - const bool Terminal::IsGridLineDrawingAllowed() noexcept { return true; diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp index d789bfc558..051732333c 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp @@ -51,8 +51,6 @@ class TerminalCoreUnitTests::TerminalBufferTests final TEST_METHOD(TestGetReverseTab); - TEST_METHOD(TestCursorNotifications); - TEST_METHOD_SETUP(MethodSetup) { // STEP 1: Set up the Terminal @@ -596,43 +594,3 @@ void TerminalBufferTests::TestGetReverseTab() L"Cursor adjusted to last item in the sample list from position beyond end."); } } - -void TerminalBufferTests::TestCursorNotifications() -{ - // Test for GH#11170 - - // Suppress test exceptions. If they occur in the lambda, they'll just crash - // TAEF, which is annoying. - const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope; - - auto callbackWasCalled = false; - auto expectedCallbacks = 0; - auto cb = [&expectedCallbacks, &callbackWasCalled]() mutable { - Log::Comment(L"Callback triggered"); - callbackWasCalled = true; - expectedCallbacks--; - VERIFY_IS_GREATER_THAN_OR_EQUAL(expectedCallbacks, 0); - }; - term->_pfnCursorPositionChanged = cb; - - // The exact number of callbacks here is fungible, if need be. - - expectedCallbacks = 1; - callbackWasCalled = false; - term->Write(L"Foo"); - VERIFY_ARE_EQUAL(0, expectedCallbacks); - VERIFY_IS_TRUE(callbackWasCalled); - - expectedCallbacks = 1; - callbackWasCalled = false; - term->Write(L"Foo\r\nBar"); - VERIFY_ARE_EQUAL(0, expectedCallbacks); - VERIFY_IS_TRUE(callbackWasCalled); - - expectedCallbacks = 2; // One for each Write - callbackWasCalled = false; - term->Write(L"Foo\r\nBar"); - term->Write(L"Foo\r\nBar"); - VERIFY_ARE_EQUAL(0, expectedCallbacks); - VERIFY_IS_TRUE(callbackWasCalled); -} diff --git a/src/host/_output.cpp b/src/host/_output.cpp index 8ad7117ea0..0804a7ffab 100644 --- a/src/host/_output.cpp +++ b/src/host/_output.cpp @@ -52,8 +52,6 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region) ServiceLocator::LocateGlobals().pRender->TriggerRedraw(region); } } - - WriteConvRegionToScreen(screenInfo, region); } // Routine Description: diff --git a/src/host/conareainfo.cpp b/src/host/conareainfo.cpp deleted file mode 100644 index 0699c7bf15..0000000000 --- a/src/host/conareainfo.cpp +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "conareainfo.h" - -#include "_output.h" - -#include "../interactivity/inc/ServiceLocator.hpp" -#include "../types/inc/viewport.hpp" - -#pragma hdrstop - -using namespace Microsoft::Console::Types; -using Microsoft::Console::Interactivity::ServiceLocator; - -ConversionAreaBufferInfo::ConversionAreaBufferInfo(const til::size coordBufferSize) : - coordCaBuffer(coordBufferSize) -{ -} - -ConversionAreaInfo::ConversionAreaInfo(const til::size bufferSize, - const til::size windowSize, - const TextAttribute& fill, - const TextAttribute& popupFill, - const FontInfo fontInfo) : - _caInfo{ bufferSize }, - _isHidden{ true }, - _screenBuffer{ nullptr } -{ - SCREEN_INFORMATION* pNewScreen = nullptr; - - // cursor has no height because it won't be rendered for conversion area - THROW_IF_NTSTATUS_FAILED(SCREEN_INFORMATION::CreateInstance(windowSize, - fontInfo, - bufferSize, - fill, - popupFill, - 0, - &pNewScreen)); - - // Suppress painting notifications for modifying a conversion area cursor as they're not actually rendered. - pNewScreen->GetTextBuffer().GetCursor().SetIsConversionArea(true); - pNewScreen->ConvScreenInfo = this; - - _screenBuffer.reset(pNewScreen); -} - -ConversionAreaInfo::ConversionAreaInfo(ConversionAreaInfo&& other) : - _caInfo(other._caInfo), - _isHidden(other._isHidden), - _screenBuffer(nullptr) -{ - std::swap(_screenBuffer, other._screenBuffer); -} - -// Routine Description: -// - Describes whether the conversion area should be drawn or should be hidden. -// Arguments: -// - -// Return Value: -// - True if it should not be drawn. False if it should be drawn. -bool ConversionAreaInfo::IsHidden() const noexcept -{ - return _isHidden; -} - -// Routine Description: -// - Sets a value describing whether the conversion area should be drawn or should be hidden. -// Arguments: -// - fIsHidden - True if it should not be drawn. False if it should be drawn. -// Return Value: -// - -void ConversionAreaInfo::SetHidden(const bool fIsHidden) noexcept -{ - _isHidden = fIsHidden; -} - -// Routine Description: -// - Retrieves the underlying text buffer for use in rendering data -const TextBuffer& ConversionAreaInfo::GetTextBuffer() const noexcept -{ - return _screenBuffer->GetTextBuffer(); -} - -// Routine Description: -// - Retrieves the layout/overlay information about where to place this conversion area relative to the -// existing screen buffers and viewports. -const ConversionAreaBufferInfo& ConversionAreaInfo::GetAreaBufferInfo() const noexcept -{ - return _caInfo; -} - -// Routine Description: -// - Forwards a color attribute setting request to the internal screen information -// Arguments: -// - attr - Color to apply to internal screen buffer -void ConversionAreaInfo::SetAttributes(const TextAttribute& attr) -{ - _screenBuffer->SetAttributes(attr); -} - -// Routine Description: -// - Writes text into the conversion area. Since conversion areas are only -// one line, you can only specify the column to write. -// Arguments: -// - text - Text to insert into the conversion area buffer -// - column - Column to start at (X position) -void ConversionAreaInfo::WriteText(const std::vector& text, - const til::CoordType column) -{ - std::span view(text.data(), text.size()); - _screenBuffer->Write(view, { column, 0 }); -} - -// Routine Description: -// - Clears out a conversion area -void ConversionAreaInfo::ClearArea() noexcept -{ - SetHidden(true); - - try - { - _screenBuffer->ClearTextData(); - } - CATCH_LOG(); - - Paint(); -} - -[[nodiscard]] HRESULT ConversionAreaInfo::Resize(const til::size newSize) noexcept -{ - // attempt to resize underlying buffers - RETURN_IF_NTSTATUS_FAILED(_screenBuffer->ResizeScreenBuffer(newSize, FALSE)); - - // store new size - _caInfo.coordCaBuffer = newSize; - - // restrict viewport to buffer size. - const til::point restriction = { newSize.width - 1, newSize.height - 1 }; - _caInfo.rcViewCaWindow.left = std::min(_caInfo.rcViewCaWindow.left, restriction.x); - _caInfo.rcViewCaWindow.right = std::min(_caInfo.rcViewCaWindow.right, restriction.x); - _caInfo.rcViewCaWindow.top = std::min(_caInfo.rcViewCaWindow.top, restriction.y); - _caInfo.rcViewCaWindow.bottom = std::min(_caInfo.rcViewCaWindow.bottom, restriction.y); - - return S_OK; -} - -void ConversionAreaInfo::SetWindowInfo(const til::inclusive_rect& view) noexcept -{ - if (view.left != _caInfo.rcViewCaWindow.left || - view.top != _caInfo.rcViewCaWindow.top || - view.right != _caInfo.rcViewCaWindow.right || - view.bottom != _caInfo.rcViewCaWindow.bottom) - { - if (!IsHidden()) - { - SetHidden(true); - Paint(); - - _caInfo.rcViewCaWindow = view; - SetHidden(false); - Paint(); - } - else - { - _caInfo.rcViewCaWindow = view; - } - } -} - -void ConversionAreaInfo::SetViewPos(const til::point pos) noexcept -{ - if (IsHidden()) - { - _caInfo.coordConView = pos; - } - else - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - auto OldRegion = _caInfo.rcViewCaWindow; - OldRegion.left += _caInfo.coordConView.x; - OldRegion.right += _caInfo.coordConView.x; - OldRegion.top += _caInfo.coordConView.y; - OldRegion.bottom += _caInfo.coordConView.y; - WriteToScreen(gci.GetActiveOutputBuffer(), Viewport::FromInclusive(OldRegion)); - - _caInfo.coordConView = pos; - - auto NewRegion = _caInfo.rcViewCaWindow; - NewRegion.left += _caInfo.coordConView.x; - NewRegion.right += _caInfo.coordConView.x; - NewRegion.top += _caInfo.coordConView.y; - NewRegion.bottom += _caInfo.coordConView.y; - WriteToScreen(gci.GetActiveOutputBuffer(), Viewport::FromInclusive(NewRegion)); - } -} - -void ConversionAreaInfo::Paint() const noexcept -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto& ScreenInfo = gci.GetActiveOutputBuffer(); - const auto viewport = ScreenInfo.GetViewport(); - - til::inclusive_rect WriteRegion; - WriteRegion.left = viewport.Left() + _caInfo.coordConView.x + _caInfo.rcViewCaWindow.left; - WriteRegion.right = WriteRegion.left + (_caInfo.rcViewCaWindow.right - _caInfo.rcViewCaWindow.left); - WriteRegion.top = viewport.Top() + _caInfo.coordConView.y + _caInfo.rcViewCaWindow.top; - WriteRegion.bottom = WriteRegion.top + (_caInfo.rcViewCaWindow.bottom - _caInfo.rcViewCaWindow.top); - - if (!IsHidden()) - { - WriteConvRegionToScreen(ScreenInfo, Viewport::FromInclusive(WriteRegion)); - } - else - { - WriteToScreen(ScreenInfo, Viewport::FromInclusive(WriteRegion)); - } -} diff --git a/src/host/conareainfo.h b/src/host/conareainfo.h deleted file mode 100644 index f334cbac81..0000000000 --- a/src/host/conareainfo.h +++ /dev/null @@ -1,74 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- conareainfo.h - -Abstract: -- This module contains the structures for the console IME conversion area -- The conversion area is the overlay on the screen where a user attempts to form - a string that they would like to insert into the buffer. - -Author: -- Michael Niksa (MiNiksa) 10-May-2018 - -Revision History: -- From pieces of convarea.cpp originally authored by KazuM ---*/ - -#pragma once - -#include "../buffer/out/OutputCell.hpp" -#include "../buffer/out/TextAttribute.hpp" -#include "../renderer/inc/FontInfo.hpp" - -class SCREEN_INFORMATION; -class TextBuffer; - -// Internal structures and definitions used by the conversion area. -class ConversionAreaBufferInfo final -{ -public: - til::size coordCaBuffer; - til::inclusive_rect rcViewCaWindow; - til::point coordConView; - - explicit ConversionAreaBufferInfo(const til::size coordBufferSize); -}; - -class ConversionAreaInfo final -{ -public: - ConversionAreaInfo(const til::size bufferSize, - const til::size windowSize, - const TextAttribute& fill, - const TextAttribute& popupFill, - const FontInfo fontInfo); - ~ConversionAreaInfo() = default; - ConversionAreaInfo(const ConversionAreaInfo&) = delete; - ConversionAreaInfo(ConversionAreaInfo&& other); - ConversionAreaInfo& operator=(const ConversionAreaInfo&) & = delete; - ConversionAreaInfo& operator=(ConversionAreaInfo&&) & = delete; - - bool IsHidden() const noexcept; - void SetHidden(const bool fIsHidden) noexcept; - void ClearArea() noexcept; - - [[nodiscard]] HRESULT Resize(const til::size newSize) noexcept; - - void SetViewPos(const til::point pos) noexcept; - void SetWindowInfo(const til::inclusive_rect& view) noexcept; - void Paint() const noexcept; - - void WriteText(const std::vector& text, const til::CoordType column); - void SetAttributes(const TextAttribute& attr); - - const TextBuffer& GetTextBuffer() const noexcept; - const ConversionAreaBufferInfo& GetAreaBufferInfo() const noexcept; - -private: - ConversionAreaBufferInfo _caInfo; - std::unique_ptr _screenBuffer; - bool _isHidden; -}; diff --git a/src/host/conimeinfo.cpp b/src/host/conimeinfo.cpp deleted file mode 100644 index fc2ce5ae74..0000000000 --- a/src/host/conimeinfo.cpp +++ /dev/null @@ -1,507 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "conimeinfo.h" - -#include - -#include "conareainfo.h" -#include "_output.h" -#include "dbcs.h" -#include "../interactivity/inc/ServiceLocator.hpp" -#include "../types/inc/GlyphWidth.hpp" - -// Attributes flags: -#define COMMON_LVB_GRID_SINGLEFLAG 0x2000 // DBCS: Grid attribute: use for ime cursor. - -using Microsoft::Console::Interactivity::ServiceLocator; - -ConsoleImeInfo::ConsoleImeInfo() : - _isSavedCursorVisible(false) -{ -} - -// Routine Description: -// - Copies default attribute (color) data from the active screen buffer into the conversion area buffers -void ConsoleImeInfo::RefreshAreaAttributes() -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto attributes = gci.GetActiveOutputBuffer().GetAttributes(); - - for (auto& area : ConvAreaCompStr) - { - area.SetAttributes(attributes); - } -} - -// Routine Description: -// - Takes the internally held composition message data from the last WriteCompMessage call -// and attempts to redraw it on the screen which will account for changes in viewport dimensions -void ConsoleImeInfo::RedrawCompMessage() -{ - if (!_text.empty()) - { - ClearAllAreas(); - _WriteUndeterminedChars(_text, _attributes, _colorArray); - } -} - -// Routine Description: -// - Writes an undetermined composition message to the screen including the text -// and color and cursor positioning attribute data so the user can walk through -// what they're proposing to insert into the buffer. -// Arguments: -// - text - The actual text of what the user would like to insert (UTF-16) -// - attributes - Encoded attributes including the cursor position and the color index (to the array) -// - colorArray - An array of colors to use for the text -void ConsoleImeInfo::WriteCompMessage(const std::wstring_view text, - const std::span attributes, - const std::span colorArray) -{ - ClearAllAreas(); - - // MSFT:29219348 only hide the cursor after the IME produces a string. - // See notes in convarea.cpp ImeStartComposition(). - SaveCursorVisibility(); - - // Save copies of the composition message in case we need to redraw it as things scroll/resize - _text = text; - _attributes.assign(attributes.begin(), attributes.end()); - _colorArray.assign(colorArray.begin(), colorArray.end()); - - _WriteUndeterminedChars(text, attributes, colorArray); -} - -// Routine Description: -// - Writes the final result into the screen buffer through the input queue -// as if the user had inputted it (if their keyboard was able to) -// Arguments: -// - text - The actual text of what the user would like to insert (UTF-16) -void ConsoleImeInfo::WriteResultMessage(const std::wstring_view text) -{ - ClearAllAreas(); - - _InsertConvertedString(text); - - _ClearComposition(); -} - -// Routine Description: -// - Clears internally cached composition data from the last WriteCompMessage call. -void ConsoleImeInfo::_ClearComposition() -{ - _text.clear(); - _attributes.clear(); - _colorArray.clear(); -} - -// Routine Description: -// - Clears out all conversion areas -void ConsoleImeInfo::ClearAllAreas() -{ - for (auto& area : ConvAreaCompStr) - { - if (!area.IsHidden()) - { - area.ClearArea(); - } - } - - // Also clear internal buffer of string data. - _ClearComposition(); -} - -// Routine Description: -// - Resizes all conversion areas to the new dimensions -// Arguments: -// - newSize - New size for conversion areas -// Return Value: -// - S_OK or appropriate failure HRESULT. -[[nodiscard]] HRESULT ConsoleImeInfo::ResizeAllAreas(const til::size newSize) -{ - for (auto& area : ConvAreaCompStr) - { - if (!area.IsHidden()) - { - area.SetHidden(true); - area.Paint(); - } - - RETURN_IF_FAILED(area.Resize(newSize)); - } - - return S_OK; -} - -// Routine Description: -// - Adds another conversion area to the current list of conversion areas (lines) available for IME candidate text -// Arguments: -// - -// Return Value: -// - Status successful or appropriate HRESULT response. -[[nodiscard]] HRESULT ConsoleImeInfo::_AddConversionArea() -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions(); - bufferSize.height = 1; - - const auto windowSize = gci.GetActiveOutputBuffer().GetViewport().Dimensions(); - - const auto fill = gci.GetActiveOutputBuffer().GetAttributes(); - - const auto popupFill = gci.GetActiveOutputBuffer().GetPopupAttributes(); - - const auto& fontInfo = gci.GetActiveOutputBuffer().GetCurrentFont(); - - try - { - ConvAreaCompStr.emplace_back(bufferSize, - windowSize, - fill, - popupFill, - fontInfo); - } - CATCH_RETURN(); - - RefreshAreaAttributes(); - - return S_OK; -} - -// Routine Description: -// - Helper method to decode the cursor and color position out of the encoded attributes -// and color array and return it in the TextAttribute structure format -// Arguments: -// - pos - Character position in the string (and matching encoded attributes array) -// - attributes - Encoded attributes holding cursor and color array position -// - colorArray - Colors to choose from -// Return Value: -// - TextAttribute object with color and cursor and line drawing data. -TextAttribute ConsoleImeInfo::s_RetrieveAttributeAt(const size_t pos, - const std::span attributes, - const std::span colorArray) -{ - // Encoded attribute is the shorthand information passed from the IME - // that contains a cursor position packed in along with which color in the - // given array should apply to the text. - auto encodedAttribute = attributes[pos]; - - // Legacy attribute is in the color/line format that is understood for drawing - // We use the lower 3 bits (0-7) from the encoded attribute as the array index to start - // creating our legacy attribute. - auto legacyAttribute = colorArray[encodedAttribute & (CONIME_ATTRCOLOR_SIZE - 1)]; - - if (WI_IsFlagSet(encodedAttribute, CONIME_CURSOR_RIGHT)) - { - WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_SINGLEFLAG); - WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_RVERTICAL); - } - else if (WI_IsFlagSet(encodedAttribute, CONIME_CURSOR_LEFT)) - { - WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_SINGLEFLAG); - WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_LVERTICAL); - } - - return TextAttribute(legacyAttribute); -} - -// Routine Description: -// - Converts IME-formatted information into OutputCells to determine what can fit into each -// displayable cell inside the console output buffer. -// Arguments: -// - text - Text data provided by the IME -// - attributes - Encoded color and cursor position data provided by the IME -// - colorArray - Array of color values provided by the IME. -// Return Value: -// - Vector of OutputCells where each one represents one cell of the output buffer. -std::vector ConsoleImeInfo::s_ConvertToCells(const std::wstring_view text, - const std::span attributes, - const std::span colorArray) -{ - std::vector cells; - - // - Walk through all of the grouped up text, match up the correct attribute to it, and make a new cell. - size_t attributesUsed = 0; - for (const auto& parsedGlyph : til::utf16_iterator{ text }) - { - const std::wstring_view glyph{ parsedGlyph.data(), parsedGlyph.size() }; - // Collect up attributes that apply to this glyph range. - auto drawingAttr = s_RetrieveAttributeAt(attributesUsed, attributes, colorArray); - attributesUsed++; - - // The IME gave us an attribute for every glyph position in a surrogate pair. - // But the only important information will be the cursor position. - // Check all additional attributes to see if the cursor resides on top of them. - for (size_t i = 1; i < glyph.size(); i++) - { - auto additionalAttr = s_RetrieveAttributeAt(attributesUsed, attributes, colorArray); - attributesUsed++; - if (additionalAttr.IsLeftVerticalDisplayed()) - { - drawingAttr.SetLeftVerticalDisplayed(true); - } - if (additionalAttr.IsRightVerticalDisplayed()) - { - drawingAttr.SetRightVerticalDisplayed(true); - } - } - - // We have to determine if the glyph range is 1 column or two. - // If it's full width, it's two, and we need to make sure we don't draw the cursor - // right down the middle of the character. - // Otherwise it's one column and we'll push it in with the default empty DbcsAttribute. - DbcsAttribute dbcsAttr = DbcsAttribute::Single; - if (IsGlyphFullWidth(glyph)) - { - auto leftHalfAttr = drawingAttr; - auto rightHalfAttr = drawingAttr; - - // Don't draw lines in the middle of full width glyphs. - // If we need a right vertical, don't apply it to the left side of the character - if (leftHalfAttr.IsRightVerticalDisplayed()) - { - leftHalfAttr.SetRightVerticalDisplayed(false); - } - - dbcsAttr = DbcsAttribute::Leading; - cells.emplace_back(glyph, dbcsAttr, leftHalfAttr); - dbcsAttr = DbcsAttribute::Trailing; - - // If we need a left vertical, don't apply it to the right side of the character - if (rightHalfAttr.IsLeftVerticalDisplayed()) - { - rightHalfAttr.SetLeftVerticalDisplayed(false); - } - cells.emplace_back(glyph, dbcsAttr, rightHalfAttr); - } - else - { - cells.emplace_back(glyph, dbcsAttr, drawingAttr); - } - } - - return cells; -} - -// Routine Description: -// - Walks through the cells given and attempts to fill a conversion area line with as much data as can fit. -// - Each conversion area represents one line of the display starting at the cursor position filling to the right edge -// of the display. -// - The first conversion area should be placed from the screen buffer's current cursor position to the right -// edge of the viewport. -// - All subsequent areas should use one entire line of the viewport. -// Arguments: -// - begin - Beginning position in OutputCells for iteration -// - end - Ending position in OutputCells for iteration -// - pos - Reference to the coordinate position in the viewport that this conversion area will occupy. -// - Updated to set up the next conversion area down a line (and to the left viewport edge) -// - view - The rectangle representing the viewable area of the screen right now to let us know how many cells can fit. -// - screenInfo - A reference to the screen information we will use for accessibility notifications -// Return Value: -// - Updated begin position for the next call. It will normally be >begin and <= end. -// However, if text couldn't fit in our line (full-width character starting at the very last cell) -// then we will give back the same begin and update the position for the next call to try again. -// If the viewport is deemed too small, we'll skip past it and advance begin past the entire full-width character. -std::vector::const_iterator ConsoleImeInfo::_WriteConversionArea(const std::vector::const_iterator begin, - const std::vector::const_iterator end, - til::point& pos, - const Microsoft::Console::Types::Viewport view, - SCREEN_INFORMATION& screenInfo) -{ - // The position in the viewport where we will start inserting cells for this conversion area - // NOTE: We might exit early if there's not enough space to fit here, so we take a copy of - // the original and increment it up front. - const auto insertionPos = pos; - - // Advance the cursor position to set up the next call for success (insert the next conversion area - // at the beginning of the following line) - pos.x = view.Left(); - pos.y++; - - // The index of the last column in the viewport. (view is inclusive) - const auto finalViewColumn = view.RightInclusive(); - - // The maximum number of cells we can insert into a line. - const auto lineWidth = finalViewColumn - insertionPos.x + 1; // +1 because view was inclusive - - // The iterator to the beginning position to form our line - const auto lineBegin = begin; - - // The total number of cells we could insert. - const auto size = end - begin; - FAIL_FAST_IF(size <= 0); // It's a programming error to have <= 0 cells to insert. - - // The end is the smaller of the remaining number of cells or the amount of line cells we can write before - // hitting the right edge of the viewport - auto lineEnd = lineBegin + std::min(size, (ptrdiff_t)lineWidth); - - // We must attempt to compensate for ending on a leading byte. We can't split a full-width character across lines. - // As such, if the last item is a leading byte, back the end up by one. - // Get the last cell in the run and if it's a leading byte, move the end position back one so we don't - // try to insert it. - const auto lastCell = lineEnd - 1; - if (lastCell->DbcsAttr() == DbcsAttribute::Leading) - { - lineEnd--; - } - - // GH#12730 - if the lineVec would now be empty, just return early. Failing - // to do so will later cause a crash trying to construct an empty view. - if (lineEnd <= lineBegin) - { - return lineEnd; - } - - // Copy out the substring into a vector. - const std::vector lineVec(lineBegin, lineEnd); - - // Add a conversion area to the internal state to hold this line. - THROW_IF_FAILED(_AddConversionArea()); - - // Get the added conversion area. - auto& area = ConvAreaCompStr.back(); - - // Write our text into the conversion area. - area.WriteText(lineVec, insertionPos.x); - - // Set the viewport and positioning parameters for the conversion area to describe to the renderer - // the appropriate location to overlay this conversion area on top of the main screen buffer inside the viewport. - const til::inclusive_rect region{ insertionPos.x, 0, gsl::narrow(insertionPos.x + lineVec.size() - 1), 0 }; - area.SetWindowInfo(region); - area.SetViewPos({ 0 - view.Left(), insertionPos.y - view.Top() }); - - // Make it visible and paint it. - area.SetHidden(false); - area.Paint(); - - // Notify accessibility that we have updated the text in this display region within the viewport. - if (screenInfo.HasAccessibilityEventing()) - { - screenInfo.NotifyAccessibilityEventing(region.left, insertionPos.y, region.right, insertionPos.y); - } - - // Hand back the iterator representing the end of what we used to be fed into the beginning of the next call. - return lineEnd; -} - -// Routine Description: -// - Takes information from the IME message to write the "undetermined" text to the -// conversion area overlays on the screen. -// - The "undetermined" text represents the word or phrase that the user is currently building -// using the IME. They haven't "determined" what they want yet, so it's "undetermined" right now. -// Arguments: -// - text - View into the text characters provided by the IME. -// - attributes - Attributes specifying which color and cursor positioning information should apply to -// each text character. This view must be the same size as the text view. -// - colorArray - 8 colors to be used to format the text for display -void ConsoleImeInfo::_WriteUndeterminedChars(const std::wstring_view text, - const std::span attributes, - const std::span colorArray) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto& screenInfo = gci.GetActiveOutputBuffer(); - - // Ensure cursor is visible for prompt line - screenInfo.MakeCurrentCursorVisible(); - - // Clear out existing conversion areas. - ConvAreaCompStr.clear(); - - // If the text length and attribute length don't match, - // it's a programming error on our part. We control the sizes here. - FAIL_FAST_IF(text.size() != attributes.size()); - - // If we have no text, return. We've already cleared above. - if (text.empty()) - { - return; - } - - // Convert data-to-be-stored into OutputCells. - const auto cells = s_ConvertToCells(text, attributes, colorArray); - - // Get some starting position information of where to place the conversion areas on top of the existing - // screen buffer and viewport positioning. - // Each conversion area write will adjust these to set up any subsequent calls to go onto the next line. - auto pos = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - // Convert the cursor buffer position to the equivalent screen - // coordinates, taking line rendition into account. - pos = screenInfo.GetTextBuffer().BufferToScreenPosition(pos); - - const auto view = screenInfo.GetViewport(); - // Set cursor position relative to viewport - - // Set up our iterators. We will walk through the entire set of cells from beginning to end. - // The first time, we will give the iterators as the whole span and the begin - // will be moved forward by the conversion area write to set up the next call. - auto begin = cells.cbegin(); - const auto end = cells.cend(); - - // Write over and over updating the beginning iterator until we reach the end. - do - { - begin = _WriteConversionArea(begin, end, pos, view, screenInfo); - } while (begin < end); -} - -// Routine Description: -// - Takes the final text string and injects it into the input buffer -// Arguments: -// - text - The text to inject into the input buffer -void ConsoleImeInfo::_InsertConvertedString(const std::wstring_view text) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - auto& screenInfo = gci.GetActiveOutputBuffer(); - if (screenInfo.GetTextBuffer().GetCursor().IsOn()) - { - gci.GetCursorBlinker().TimerRoutine(screenInfo); - } - - const auto dwControlKeyState = GetControlKeyState(0); - InputEventQueue inEvents; - auto keyEvent = SynthesizeKeyEvent(true, 1, 0, 0, 0, dwControlKeyState); - - for (const auto& ch : text) - { - keyEvent.Event.KeyEvent.uChar.UnicodeChar = ch; - inEvents.push_back(keyEvent); - } - - gci.pInputBuffer->Write(inEvents); -} - -// Routine Description: -// - Backs up the global cursor visibility state if it is shown and disables -// it while we work on the conversion areas. -void ConsoleImeInfo::SaveCursorVisibility() -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - - // Cursor turn OFF. - if (cursor.IsVisible()) - { - _isSavedCursorVisible = true; - - cursor.SetIsVisible(false); - } -} - -// Routine Description: -// - Restores the global cursor visibility state if it was on when it was backed up. -void ConsoleImeInfo::RestoreCursorVisibility() -{ - if (_isSavedCursorVisible) - { - _isSavedCursorVisible = false; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - - cursor.SetIsVisible(true); - } -} diff --git a/src/host/conimeinfo.h b/src/host/conimeinfo.h deleted file mode 100644 index 81ea359d8a..0000000000 --- a/src/host/conimeinfo.h +++ /dev/null @@ -1,91 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- conimeinfo.h - -Abstract: -- This module contains the structures for the console IME entrypoints - for overall control - -Author: -- Michael Niksa (MiNiksa) 10-May-2018 - -Revision History: -- From pieces of convarea.cpp originally authored by KazuM ---*/ - -#pragma once - -#include "../inc/conime.h" -#include "../buffer/out/OutputCell.hpp" -#include "../buffer/out/TextAttribute.hpp" -#include "../renderer/inc/FontInfo.hpp" -#include "../types/inc/viewport.hpp" - -#include "conareainfo.h" - -class SCREEN_INFORMATION; - -class ConsoleImeInfo final -{ -public: - // IME composition string information - // There is one "composition string" per line that must be rendered on the screen - std::vector ConvAreaCompStr; - - ConsoleImeInfo(); - ~ConsoleImeInfo() = default; - ConsoleImeInfo(const ConsoleImeInfo&) = delete; - ConsoleImeInfo(ConsoleImeInfo&&) = delete; - ConsoleImeInfo& operator=(const ConsoleImeInfo&) & = delete; - ConsoleImeInfo& operator=(ConsoleImeInfo&&) & = delete; - - void RefreshAreaAttributes(); - void ClearAllAreas(); - - [[nodiscard]] HRESULT ResizeAllAreas(const til::size newSize); - - void WriteCompMessage(const std::wstring_view text, - const std::span attributes, - const std::span colorArray); - - void WriteResultMessage(const std::wstring_view text); - - void RedrawCompMessage(); - - void SaveCursorVisibility(); - void RestoreCursorVisibility(); - -private: - [[nodiscard]] HRESULT _AddConversionArea(); - - void _ClearComposition(); - - void _WriteUndeterminedChars(const std::wstring_view text, - const std::span attributes, - const std::span colorArray); - - void _InsertConvertedString(const std::wstring_view text); - - static TextAttribute s_RetrieveAttributeAt(const size_t pos, - const std::span attributes, - const std::span colorArray); - - static std::vector s_ConvertToCells(const std::wstring_view text, - const std::span attributes, - const std::span colorArray); - - std::vector::const_iterator _WriteConversionArea(const std::vector::const_iterator begin, - const std::vector::const_iterator end, - til::point& pos, - const Microsoft::Console::Types::Viewport view, - SCREEN_INFORMATION& screenInfo); - - bool _isSavedCursorVisible; - - std::wstring _text; - std::vector _attributes; - std::vector _colorArray; -}; diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index 074bd56911..12e8c5e5b5 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -102,8 +102,6 @@ ULONG CONSOLE_INFORMATION::GetCSRecursionCount() const noexcept gci.GetActiveOutputBuffer().ScrollScale = gci.GetScrollScale(); - gci.ConsoleIme.RefreshAreaAttributes(); - if (SUCCEEDED_NTSTATUS(Status)) { return STATUS_SUCCESS; diff --git a/src/host/conv.h b/src/host/conv.h deleted file mode 100644 index fb1a02395e..0000000000 --- a/src/host/conv.h +++ /dev/null @@ -1,29 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- conv.h - -Abstract: -- This module contains the internal structures and definitions used by the conversion area. -- "Conversion area" refers to either the in-line area where the text color changes and suggests options in IME-based languages - or to the reserved line at the bottom of the screen offering suggestions and the current IME mode. - -Author: -- KazuM March 8, 1993 - -Revision History: ---*/ - -#pragma once - -#include "server.h" - -#include "../types/inc/Viewport.hpp" - -void WriteConvRegionToScreen(const SCREEN_INFORMATION& ScreenInfo, - const Microsoft::Console::Types::Viewport& convRegion); - -[[nodiscard]] HRESULT ConsoleImeResizeCompStrView(); -[[nodiscard]] HRESULT ConsoleImeResizeCompStrScreenBuffer(const til::size coordNewScreenSize); diff --git a/src/host/convarea.cpp b/src/host/convarea.cpp deleted file mode 100644 index 62c9796a56..0000000000 --- a/src/host/convarea.cpp +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "_output.h" - -#include "../interactivity/inc/ServiceLocator.hpp" - -#pragma hdrstop - -using namespace Microsoft::Console::Types; -using Microsoft::Console::Interactivity::ServiceLocator; - -void WriteConvRegionToScreen(const SCREEN_INFORMATION& ScreenInfo, - const Viewport& convRegion) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (!ScreenInfo.IsActiveScreenBuffer()) - { - return; - } - - const auto pIme = &gci.ConsoleIme; - - for (unsigned int i = 0; i < pIme->ConvAreaCompStr.size(); ++i) - { - const auto& ConvAreaInfo = pIme->ConvAreaCompStr[i]; - - if (!ConvAreaInfo.IsHidden()) - { - const auto currentViewport = ScreenInfo.GetViewport().ToInclusive(); - const auto areaInfo = ConvAreaInfo.GetAreaBufferInfo(); - - // Do clipping region - til::inclusive_rect Region; - Region.left = currentViewport.left + areaInfo.rcViewCaWindow.left + areaInfo.coordConView.x; - Region.right = Region.left + (areaInfo.rcViewCaWindow.right - areaInfo.rcViewCaWindow.left); - Region.top = currentViewport.top + areaInfo.rcViewCaWindow.top + areaInfo.coordConView.y; - Region.bottom = Region.top + (areaInfo.rcViewCaWindow.bottom - areaInfo.rcViewCaWindow.top); - - Region.left = std::max(Region.left, currentViewport.left); - Region.top = std::max(Region.top, currentViewport.top); - Region.right = std::min(Region.right, currentViewport.right); - Region.bottom = std::min(Region.bottom, currentViewport.bottom); - - if (Region) - { - Region.left = std::max(Region.left, convRegion.Left()); - Region.top = std::max(Region.top, convRegion.Top()); - Region.right = std::min(Region.right, convRegion.RightInclusive()); - Region.bottom = std::min(Region.bottom, convRegion.BottomInclusive()); - if (Region) - { - // if we have a renderer, we need to update. - // we've already confirmed (above with an early return) that we're on conversion areas that are a part of the active (visible/rendered) screen - // so send invalidates to those regions such that we're queried for data on the next frame and repainted. - if (ServiceLocator::LocateGlobals().pRender != nullptr) - { - ServiceLocator::LocateGlobals().pRender->TriggerRedraw(Viewport::FromInclusive(Region)); - } - } - } - } - } -} - -[[nodiscard]] HRESULT ConsoleImeResizeCompStrView() -{ - try - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto pIme = &gci.ConsoleIme; - pIme->RedrawCompMessage(); - } - CATCH_RETURN(); - - return S_OK; -} - -[[nodiscard]] HRESULT ConsoleImeResizeCompStrScreenBuffer(const til::size coordNewScreenSize) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto pIme = &gci.ConsoleIme; - - return pIme->ResizeAllAreas(coordNewScreenSize); -} - -[[nodiscard]] HRESULT ImeStartComposition() -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.LockConsole(); - auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); - - // MSFT:29219348 Some IME implementations do not produce composition strings, and - // their users have come to rely on the cursor that conhost traditionally left on - // until a composition string showed up. - // One such IME is WNWB's "Universal Wubi input method" from wnwb.com (v. 10+). - // We shouldn't hide the cursor here so as to not break those IMEs. - - gci.pInputBuffer->fInComposition = true; - return S_OK; -} - -[[nodiscard]] HRESULT ImeEndComposition() -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.LockConsole(); - auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); - - const auto pIme = &gci.ConsoleIme; - pIme->RestoreCursorVisibility(); - - gci.pInputBuffer->fInComposition = false; - return S_OK; -} - -[[nodiscard]] HRESULT ImeComposeData(std::wstring_view text, - std::span attributes, - std::span colorArray) -{ - try - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.LockConsole(); - auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); - - const auto pIme = &gci.ConsoleIme; - pIme->WriteCompMessage(text, attributes, colorArray); - } - CATCH_RETURN(); - return S_OK; -} - -[[nodiscard]] HRESULT ImeClearComposeData() -{ - try - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.LockConsole(); - auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); - - const auto pIme = &gci.ConsoleIme; - pIme->ClearAllAreas(); - } - CATCH_RETURN(); - return S_OK; -} - -[[nodiscard]] HRESULT ImeComposeResult(std::wstring_view text) -{ - try - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.LockConsole(); - auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); - - const auto pIme = &gci.ConsoleIme; - pIme->WriteResultMessage(text); - } - CATCH_RETURN(); - return S_OK; -} diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 304b4fd65c..3595da335c 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -717,8 +717,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont RETURN_IF_NTSTATUS_FAILED(buffer.SetCursorPosition(position, true)); - LOG_IF_FAILED(ConsoleImeResizeCompStrView()); - // Attempt to "snap" the viewport to the cursor position. If the cursor // is not in the current viewport, we'll try and move the viewport so // that the cursor is visible. @@ -976,7 +974,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont [[nodiscard]] HRESULT ApiRoutines::SetConsoleTextAttributeImpl(SCREEN_INFORMATION& context, const WORD attribute) noexcept { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); try { LockConsole(); @@ -987,8 +984,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont const TextAttribute attr{ attribute }; context.SetAttributes(attr); - gci.ConsoleIme.RefreshAreaAttributes(); - return S_OK; } CATCH_RETURN(); diff --git a/src/host/globals.h b/src/host/globals.h index 9cebbc0808..812bf059c4 100644 --- a/src/host/globals.h +++ b/src/host/globals.h @@ -25,6 +25,7 @@ Revision History: #include "../propslib/DelegationConfig.hpp" #include "../renderer/base/Renderer.hpp" #include "../server/DeviceComm.h" +#include "../tsf/Handle.h" #include "../server/ConDrvDeviceComm.h" #include @@ -60,9 +61,8 @@ public: DWORD dwInputThreadId; std::vector WordDelimiters; - Microsoft::Console::Render::Renderer* pRender; - + Microsoft::Console::TSF::Handle tsf; Microsoft::Console::Render::IFontDefaultList* pFontDefaultList; bool IsHeadless() const; diff --git a/src/host/host-common.vcxitems b/src/host/host-common.vcxitems index 3456180ff1..9e51ab38c8 100644 --- a/src/host/host-common.vcxitems +++ b/src/host/host-common.vcxitems @@ -6,10 +6,7 @@ - - - @@ -58,11 +55,8 @@ - - - diff --git a/src/host/inputBuffer.cpp b/src/host/inputBuffer.cpp index 2e28a17ef6..a843a01082 100644 --- a/src/host/inputBuffer.cpp +++ b/src/host/inputBuffer.cpp @@ -27,8 +27,6 @@ using namespace Microsoft::Console; InputBuffer::InputBuffer() : InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE } { - // initialize buffer header - fInComposition = false; } // Transfer as many `wchar_t`s from source over to the `char`/`wchar_t` buffer `target`. After it returns, diff --git a/src/host/inputBuffer.hpp b/src/host/inputBuffer.hpp index 019c6e628b..653f04e36f 100644 --- a/src/host/inputBuffer.hpp +++ b/src/host/inputBuffer.hpp @@ -23,7 +23,6 @@ class InputBuffer final : public ConsoleObjectHeader public: DWORD InputMode; ConsoleWaitQueue WaitQueue; // formerly ReadWaitQueue - bool fInComposition; // specifies if there's an ongoing text composition InputBuffer(); diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters index c70ea1a4d9..a2e17a06cd 100644 --- a/src/host/lib/hostlib.vcxproj.filters +++ b/src/host/lib/hostlib.vcxproj.filters @@ -27,9 +27,6 @@ Source Files - - Source Files - Source Files @@ -105,9 +102,6 @@ Source Files - - Source Files - Source Files @@ -150,9 +144,6 @@ Source Files - - Source Files - Source Files @@ -191,9 +182,6 @@ Header Files - - Header Files - Header Files @@ -215,15 +203,9 @@ Header Files - - Header Files - Header Files - - Header Files - Header Files @@ -299,9 +281,6 @@ Header Files - - Header Files - Header Files @@ -313,4 +292,4 @@ - + \ No newline at end of file diff --git a/src/host/output.cpp b/src/host/output.cpp index bf1e0a9104..1958e0671a 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -464,8 +464,6 @@ void SetActiveScreenBuffer(SCREEN_INFORMATION& screenInfo) // Set window size. screenInfo.PostUpdateWindowSize(); - gci.ConsoleIme.RefreshAreaAttributes(); - // Write data to screen. WriteToScreen(screenInfo, screenInfo.GetViewport()); } diff --git a/src/host/precomp.h b/src/host/precomp.h index e28931ebaf..11787fc7b1 100644 --- a/src/host/precomp.h +++ b/src/host/precomp.h @@ -48,8 +48,6 @@ Abstract: #include #include "conserv.h" -#include "conv.h" - #pragma prefast(push) #pragma prefast(disable : 26071, "Range violation in Intsafe. Not ours.") #define ENABLE_INTSAFE_SIGNED_FUNCTIONS // Only unsigned intsafe math/casts available without this def @@ -84,7 +82,6 @@ TRACELOGGING_DECLARE_PROVIDER(g_hConhostV2EventTraceProvider); #define CON_DPIAPI_INDIRECT #endif -#include "../inc/contsf.h" #include "../inc/conattrs.hpp" // TODO: MSFT 9355094 Find a better way of doing this. http://osgvsowi/9355094 diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index c2ad7e99d5..f242f2753f 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -42,9 +42,9 @@ til::point RenderData::GetTextBufferEndPosition() const noexcept // the appropriate windowing. // Return Value: // - Text buffer with cell information for display -const TextBuffer& RenderData::GetTextBuffer() const noexcept +TextBuffer& RenderData::GetTextBuffer() const noexcept { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return gci.GetActiveOutputBuffer().GetTextBuffer(); } @@ -210,47 +210,6 @@ ULONG RenderData::GetCursorPixelWidth() const noexcept return ServiceLocator::LocateGlobals().cursorPixelWidth; } -// Routine Description: -// - Retrieves overlays to be drawn on top of the main screen buffer area. -// - Overlays are drawn from first to last -// (the highest overlay should be given last) -// Return Value: -// - Iterable set of overlays -const std::vector RenderData::GetOverlays() const noexcept -{ - std::vector overlays; - - try - { - // First retrieve the IME information and build overlays. - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& ime = gci.ConsoleIme; - - for (const auto& composition : ime.ConvAreaCompStr) - { - // Only send the overlay to the renderer on request if it's not supposed to be hidden at this moment. - if (!composition.IsHidden()) - { - // This is holding the data. - const auto& textBuffer = composition.GetTextBuffer(); - - // The origin of the text buffer above (top left corner) is supposed to sit at this - // point within the visible viewport of the current window. - const auto origin = composition.GetAreaBufferInfo().coordConView; - - // This is the area of the viewport that is actually in use relative to the text buffer itself. - // (e.g. 0,0 is the origin of the text buffer above, not the placement within the visible viewport) - const auto used = Viewport::FromInclusive(composition.GetAreaBufferInfo().rcViewCaWindow); - - overlays.emplace_back(Microsoft::Console::Render::RenderOverlay{ textBuffer, origin, used }); - } - } - } - CATCH_LOG(); - - return overlays; -} - // Method Description: // - Returns true if the cursor should be drawn twice as wide as usual because // the cursor is currently over a cell with a double-wide character in it. diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index db879fb54e..839366b494 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -22,7 +22,7 @@ class RenderData final : public: Microsoft::Console::Types::Viewport GetViewport() noexcept override; til::point GetTextBufferEndPosition() const noexcept override; - const TextBuffer& GetTextBuffer() const noexcept override; + TextBuffer& GetTextBuffer() const noexcept override; const FontInfo& GetFontInfo() const noexcept override; std::vector GetSelectionRects() noexcept override; @@ -38,8 +38,6 @@ public: ULONG GetCursorPixelWidth() const noexcept override; bool IsCursorDoubleWidth() const override; - const std::vector GetOverlays() const noexcept override; - const bool IsGridLineDrawingAllowed() noexcept override; const std::wstring_view GetConsoleTitle() const noexcept override; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 9b0ff2ca4e..daaedf87d7 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -40,7 +40,6 @@ SCREEN_INFORMATION::SCREEN_INFORMATION( Next{ nullptr }, WriteConsoleDbcsLeadByte{ 0, 0 }, FillOutDbcsLeadChar{ 0 }, - ConvScreenInfo{ nullptr }, ScrollScale{ 1ul }, _pConsoleWindowMetrics{ pMetrics }, _pAccessibilityNotifier{ pNotifier }, @@ -1496,15 +1495,6 @@ NT_CATCH_RETURN() NotifyAccessibilityEventing(0, 0, coordNewScreenSize.width - 1, coordNewScreenSize.height - 1); } - if ((!ConvScreenInfo)) - { - if (FAILED(ConsoleImeResizeCompStrScreenBuffer(coordNewScreenSize))) - { - // If something went wrong, just bail out. - return STATUS_INVALID_HANDLE; - } - } - // Fire off an event to let accessibility apps know the layout has changed. if (_pAccessibilityNotifier && IsActiveScreenBuffer()) { @@ -2114,8 +2104,6 @@ void SCREEN_INFORMATION::SetDefaultAttributes(const TextAttribute& attributes, _textBuffer->TriggerRedrawAll(); } - gci.ConsoleIme.RefreshAreaAttributes(); - // If we're an alt buffer, also update our main buffer. if (_psiMainBuffer) { diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index c035105056..035529e0d6 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -176,9 +176,6 @@ public: BYTE WriteConsoleDbcsLeadByte[2]; BYTE FillOutDbcsLeadChar; - // non ownership pointer - ConversionAreaInfo* ConvScreenInfo; - UINT ScrollScale; bool IsActiveScreenBuffer() const; diff --git a/src/host/server.h b/src/host/server.h index cfa1ba14dc..0cf24ef42f 100644 --- a/src/host/server.h +++ b/src/host/server.h @@ -16,7 +16,6 @@ Revision History: #pragma once -#include "conimeinfo.h" #include "CursorBlinker.hpp" #include "IIoProvider.hpp" #include "readDataCooked.hpp" @@ -98,8 +97,6 @@ public: CPINFO CPInfo = {}; CPINFO OutputCPInfo = {}; - ConsoleImeInfo ConsoleIme; - void LockConsole() noexcept; void UnlockConsole() noexcept; til::recursive_ticket_lock_suspension SuspendLock() noexcept; diff --git a/src/host/sources.inc b/src/host/sources.inc index 0c6b8fd14d..adf9ee9fb4 100644 --- a/src/host/sources.inc +++ b/src/host/sources.inc @@ -67,7 +67,6 @@ SOURCES = \ ..\outputStream.cpp \ ..\stream.cpp \ ..\dbcs.cpp \ - ..\convarea.cpp \ ..\screenInfo.cpp \ ..\_output.cpp \ ..\_stream.cpp \ @@ -83,8 +82,6 @@ SOURCES = \ ..\writeData.cpp \ ..\renderData.cpp \ ..\renderFontDefaults.cpp \ - ..\conareainfo.cpp \ - ..\conimeinfo.cpp \ ..\ConsoleArguments.cpp \ diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index dbabcc5a28..4644a4deb5 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -259,7 +259,7 @@ public: return {}; } - const TextBuffer& GetTextBuffer() const noexcept override + TextBuffer& GetTextBuffer() const noexcept override { FAIL_FAST_HR(E_NOTIMPL); } @@ -322,11 +322,6 @@ public: return false; } - const std::vector GetOverlays() const noexcept override - { - return std::vector{}; - } - const bool IsGridLineDrawingAllowed() noexcept override { return false; diff --git a/src/inc/conime.h b/src/inc/conime.h deleted file mode 100644 index 96ca7c3e0b..0000000000 --- a/src/inc/conime.h +++ /dev/null @@ -1,58 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - conime.h - -Abstract: - - This module contains the internal structures and definitions used - by the console IME. - -Author: - - v-HirShi Jul.4.1995 - -Revision History: - ---*/ - -#pragma once - -constexpr unsigned short CONIME_ATTRCOLOR_SIZE = 8; - -constexpr BYTE CONIME_CURSOR_RIGHT = 0x10; -constexpr BYTE CONIME_CURSOR_LEFT = 0x20; - -[[nodiscard]] HRESULT ImeStartComposition(); - -[[nodiscard]] HRESULT ImeEndComposition(); - -[[nodiscard]] HRESULT ImeComposeData(std::wstring_view text, - std::span attributes, - std::span colorArray); - -[[nodiscard]] HRESULT ImeClearComposeData(); - -[[nodiscard]] HRESULT ImeComposeResult(std::wstring_view text); - -// Default composition color attributes -#define DEFAULT_COMP_ENTERED \ - (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | \ - COMMON_LVB_UNDERSCORE) -#define DEFAULT_COMP_ALREADY_CONVERTED \ - (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | \ - BACKGROUND_BLUE) -#define DEFAULT_COMP_CONVERSION \ - (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | \ - COMMON_LVB_UNDERSCORE) -#define DEFAULT_COMP_YET_CONVERTED \ - (FOREGROUND_BLUE | \ - BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | \ - COMMON_LVB_UNDERSCORE) -#define DEFAULT_COMP_INPUT_ERROR \ - (FOREGROUND_RED | \ - COMMON_LVB_UNDERSCORE) diff --git a/src/inc/contsf.h b/src/inc/contsf.h deleted file mode 100644 index 07594e104e..0000000000 --- a/src/inc/contsf.h +++ /dev/null @@ -1,40 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - contsf.h - -Abstract: - - This module contains the internal structures and definitions used - by the console IME. - -Author: - - v-HirShi Jul.4.1995 - -Revision History: - ---*/ - -#pragma once - -#include "conime.h" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef RECT (*GetSuggestionWindowPos)(); -typedef RECT (*GetTextBoxAreaPos)(); - -BOOL ActivateTextServices(HWND hwndConsole, GetSuggestionWindowPos pfnPosition, GetTextBoxAreaPos pfnTextArea); -void DeactivateTextServices(); -BOOL NotifyTextServices(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* lplResult); - -#ifdef __cplusplus -} -#endif diff --git a/src/interactivity/win32/CustomWindowMessages.h b/src/interactivity/win32/CustomWindowMessages.h index c203df2cac..bf36660249 100644 --- a/src/interactivity/win32/CustomWindowMessages.h +++ b/src/interactivity/win32/CustomWindowMessages.h @@ -15,7 +15,7 @@ // unused (CM_CONSOLE_SHUTDOWN) (WM_USER + 7) // unused (CM_HIDE_WINDOW) (WM_USER + 8) #define CM_CONIME_CREATE (WM_USER+9) -#define CM_SET_CONSOLEIME_WINDOW (WM_USER+10) +// unused #define CM_SET_CONSOLEIME_WINDOW (WM_USER+10) #define CM_WAIT_CONIME_PROCESS (WM_USER+11) // unused CM_SET_IME_CODEPAGE (WM_USER+12) // unused CM_SET_NLSMODE (WM_USER+13) diff --git a/src/interactivity/win32/WindowIme.cpp b/src/interactivity/win32/WindowIme.cpp deleted file mode 100644 index f66338d7de..0000000000 --- a/src/interactivity/win32/WindowIme.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "../inc/ServiceLocator.hpp" - -#include "window.hpp" - -#pragma hdrstop - -// Routine Description: -// - This method gives a rectangle to where the command edit line text is currently rendered -// such that the IME suggestion window can pop up in a suitable location adjacent to the given rectangle. -// Arguments: -// - -// Return Value: -// - Rectangle specifying current command line edit area. -RECT GetImeSuggestionWindowPos() -{ - using Microsoft::Console::Interactivity::ServiceLocator; - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& screenBuffer = gci.GetActiveOutputBuffer(); - - const auto coordFont = screenBuffer.GetCurrentFont().GetSize(); - auto coordCursor = screenBuffer.GetTextBuffer().GetCursor().GetPosition(); - - // Adjust the cursor position to be relative to the viewport. - // This means that if the cursor is at row 30 in the buffer but the viewport is showing rows 20-40 right now on screen - // that the "relative" position is that it is on the 11th line from the top (or 10th by index). - // Correct by subtracting the top/left corner from the cursor's position. - const auto srViewport = screenBuffer.GetViewport().ToInclusive(); - coordCursor.x -= srViewport.left; - coordCursor.y -= srViewport.top; - - // Map the point to be just under the current cursor position. Convert from coordinate to pixels using font. - POINT ptSuggestion; - ptSuggestion.x = (coordCursor.x + 1) * coordFont.width; - ptSuggestion.y = (coordCursor.y) * coordFont.height; - - // Adjust client point to screen point via HWND. - ClientToScreen(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), &ptSuggestion); - - // Move into suggestion rectangle. - RECT rcSuggestion{}; - rcSuggestion.top = rcSuggestion.bottom = ptSuggestion.y; - rcSuggestion.left = rcSuggestion.right = ptSuggestion.x; - - // Add 1 line height and a few characters of width to represent the area where we're writing text. - // This could be more exact by looking up the CONVAREA but it works well enough this way. - // If there is a future issue with the pop-up window, tweak these metrics. - rcSuggestion.bottom += coordFont.height; - rcSuggestion.right += (coordFont.width * 10); - - return rcSuggestion; -} - -// Routine Description: -// - This method gives a rectangle to where text box is currently rendered -// such that the touch keyboard can pop up when the rectangle is tapped. -// Arguments: -// - -// Return Value: -// - Rectangle specifying current text box area. -RECT GetTextBoxArea() -{ - return Microsoft::Console::Interactivity::ServiceLocator::LocateConsoleWindow()->GetWindowRect().to_win32_rect(); -} diff --git a/src/interactivity/win32/lib/win32.LIB.vcxproj b/src/interactivity/win32/lib/win32.LIB.vcxproj index 1988841434..6d6df33f5f 100644 --- a/src/interactivity/win32/lib/win32.LIB.vcxproj +++ b/src/interactivity/win32/lib/win32.LIB.vcxproj @@ -32,7 +32,6 @@ - @@ -54,7 +53,6 @@ - diff --git a/src/interactivity/win32/lib/win32.LIB.vcxproj.filters b/src/interactivity/win32/lib/win32.LIB.vcxproj.filters index 52ecd14f5b..afe3fd0fba 100644 --- a/src/interactivity/win32/lib/win32.LIB.vcxproj.filters +++ b/src/interactivity/win32/lib/win32.LIB.vcxproj.filters @@ -48,9 +48,6 @@ Source Files - - Source Files - Source Files @@ -110,9 +107,6 @@ Header Files - - Header Files - Header Files diff --git a/src/interactivity/win32/menu.cpp b/src/interactivity/win32/menu.cpp index 44e803931f..e9736728a7 100644 --- a/src/interactivity/win32/menu.cpp +++ b/src/interactivity/win32/menu.cpp @@ -595,8 +595,6 @@ void Menu::s_PropertiesUpdate(PCONSOLE_STATE_INFO pStateInfo) // those properties specifically from the registry in case they were changed. ServiceLocator::LocateConsoleWindow()->PostUpdateExtendedEditKeys(); - gci.ConsoleIme.RefreshAreaAttributes(); - gci.SetInterceptCopyPaste(!!pStateInfo->InterceptCopyPaste); } diff --git a/src/interactivity/win32/sources.inc b/src/interactivity/win32/sources.inc index 3580a5d618..f816c285db 100644 --- a/src/interactivity/win32/sources.inc +++ b/src/interactivity/win32/sources.inc @@ -50,7 +50,6 @@ SOURCES = \ ..\UiaTextRange.cpp \ ..\window.cpp \ ..\windowdpiapi.cpp \ - ..\windowime.cpp \ ..\windowio.cpp \ ..\WindowMetrics.cpp \ ..\windowproc.cpp \ diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index 3ef07add74..ed42acb60d 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -334,8 +334,6 @@ void Window::_UpdateSystemMetrics() const if (SUCCEEDED_NTSTATUS(status)) { - gci.ConsoleIme.RefreshAreaAttributes(); - // Do WM_GETICON workaround. Must call WM_SETICON once or apps calling WM_GETICON will get null. LOG_IF_FAILED(Icon::Instance().ApplyWindowMessageWorkaround(hWnd)); @@ -455,8 +453,6 @@ void Window::ChangeViewport(const til::inclusive_rect& NewWindow) Tracing::s_TraceWindowViewport(ScreenInfo.GetViewport()); } - LOG_IF_FAILED(ConsoleImeResizeCompStrView()); - ScreenInfo.UpdateScrollBars(); } @@ -686,8 +682,6 @@ void Window::_UpdateWindowSize(const til::size sizeNew) _resizingWindow--; } - LOG_IF_FAILED(ConsoleImeResizeCompStrView()); - return STATUS_SUCCESS; } diff --git a/src/interactivity/win32/windowime.hpp b/src/interactivity/win32/windowime.hpp deleted file mode 100644 index dc7e6d241d..0000000000 --- a/src/interactivity/win32/windowime.hpp +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#pragma hdrstop - -RECT GetImeSuggestionWindowPos(); -RECT GetTextBoxArea(); diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index 94d7f3b2a8..261ed4a80c 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -2,22 +2,16 @@ // Licensed under the MIT license. #include "precomp.h" - #include "windowio.hpp" -#include "ConsoleControl.hpp" -#include "find.h" #include "clipboard.hpp" +#include "ConsoleControl.hpp" #include "consoleKeyInfo.hpp" +#include "find.h" #include "window.hpp" - -#include "../../host/ApiRoutines.h" -#include "../../host/init.hpp" -#include "../../host/input.h" #include "../../host/handle.h" +#include "../../host/init.hpp" #include "../../host/scrolling.hpp" -#include "../../host/output.h" - #include "../inc/ServiceLocator.hpp" #pragma hdrstop @@ -125,7 +119,8 @@ void HandleKeyEvent(const HWND hWnd, const LPARAM lParam, _Inout_opt_ PBOOL pfUnlockConsole) { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& g = ServiceLocator::LocateGlobals(); + auto& gci = g.getConsoleInformation(); // BOGUS for WM_CHAR/WM_DEADCHAR, in which LOWORD(wParam) is a character auto VirtualKeyCode = LOWORD(wParam); @@ -153,7 +148,7 @@ void HandleKeyEvent(const HWND hWnd, RetrieveKeyInfo(hWnd, &VirtualKeyCode, &VirtualScanCode, - !gci.pInputBuffer->fInComposition); + !g.tsf.HasActiveComposition()); // --- END LOAD BEARING CODE --- } @@ -363,7 +358,7 @@ void HandleKeyEvent(const HWND hWnd, return; } - if (gci.pInputBuffer->fInComposition) + if (g.tsf.HasActiveComposition()) { return; } @@ -1003,9 +998,6 @@ DWORD WINAPI ConsoleInputThreadProcWin32(LPVOID /*lpParameter*/) // -- END LOAD BEARING CODE } - // Free all resources used by this thread - DeactivateTextServices(); - if (nullptr != hhook) { UnhookWindowsHookEx(hhook); diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index 28202eebc9..0dc8cf3853 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -2,41 +2,116 @@ // Licensed under the MIT license. #include "precomp.h" +#include "window.hpp" -#include "Clipboard.hpp" -#include "ConsoleControl.hpp" +#include "clipboard.hpp" #include "find.h" #include "menu.hpp" -#include "window.hpp" #include "windowdpiapi.hpp" -#include "windowime.hpp" #include "windowio.hpp" #include "windowmetrics.hpp" - -#include "../../host/_output.h" -#include "../../host/output.h" -#include "../../host/dbcs.h" #include "../../host/handle.h" -#include "../../host/input.h" -#include "../../host/misc.h" #include "../../host/registry.hpp" #include "../../host/scrolling.hpp" -#include "../../host/srvinit.h" - -#include "../inc/ServiceLocator.hpp" - #include "../../inc/conint.h" - +#include "../inc/ServiceLocator.hpp" #include "../interactivity/win32/CustomWindowMessages.h" - #include "../interactivity/win32/windowUiaProvider.hpp" -#include -#include - using namespace Microsoft::Console::Interactivity::Win32; using namespace Microsoft::Console::Types; +// NOTE: We put this struct into a `static constexpr` (= ".rodata", read-only data segment), which means it +// cannot have any mutable members right now. If you need any, you have to make it a non-const `static`. +struct TsfDataProvider : Microsoft::Console::TSF::IDataProvider +{ + virtual ~TsfDataProvider() = default; + + STDMETHODIMP TsfDataProvider::QueryInterface(REFIID, void**) noexcept override + { + return E_NOTIMPL; + } + + ULONG STDMETHODCALLTYPE TsfDataProvider::AddRef() noexcept override + { + return 1; + } + + ULONG STDMETHODCALLTYPE TsfDataProvider::Release() noexcept override + { + return 1; + } + + HWND GetHwnd() override + { + return Microsoft::Console::Interactivity::ServiceLocator::LocateConsoleWindow()->GetWindowHandle(); + } + + RECT GetViewport() override + { + const auto hwnd = GetHwnd(); + + RECT rc; + GetClientRect(hwnd, &rc); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclientrect + // > The left and top members are zero. The right and bottom members contain the width and height of the window. + // --> We can turn the client rect into a screen-relative rect by adding the left/top position. + ClientToScreen(hwnd, reinterpret_cast(&rc)); + rc.right += rc.left; + rc.bottom += rc.top; + + return rc; + } + + RECT GetCursorPosition() override + { + const auto& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& screenBuffer = gci.GetActiveOutputBuffer(); + + // Map the absolute cursor position to a viewport-relative one. + const auto viewport = screenBuffer.GetViewport().ToExclusive(); + auto coordCursor = screenBuffer.GetTextBuffer().GetCursor().GetPosition(); + coordCursor.x -= viewport.left; + coordCursor.y -= viewport.top; + + coordCursor.x = std::clamp(coordCursor.x, 0, viewport.width() - 1); + coordCursor.y = std::clamp(coordCursor.y, 0, viewport.height() - 1); + + // Convert from columns/rows to pixels. + const auto coordFont = screenBuffer.GetCurrentFont().GetSize(); + POINT ptSuggestion{ + .x = coordCursor.x * coordFont.width, + .y = coordCursor.y * coordFont.height, + }; + + ClientToScreen(GetHwnd(), &ptSuggestion); + + return { + .left = ptSuggestion.x, + .top = ptSuggestion.y, + .right = ptSuggestion.x + coordFont.width, + .bottom = ptSuggestion.y + coordFont.height, + }; + } + + void HandleOutput(std::wstring_view text) override + { + auto& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + const auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + gci.GetActiveInputBuffer()->WriteString(text); + } + + Microsoft::Console::Render::Renderer* GetRenderer() override + { + auto& g = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals(); + return g.pRender; + } +}; + +static constexpr TsfDataProvider s_tsfDataProvider; + // The static and specific window procedures for this class are contained here #pragma region Window Procedure @@ -256,8 +331,11 @@ using namespace Microsoft::Console::Types; HandleFocusEvent(TRUE); - // ActivateTextServices does nothing if already active so this is OK to be called every focus. - ActivateTextServices(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), GetImeSuggestionWindowPos, GetTextBoxArea); + if (!g.tsf) + { + g.tsf = TSF::Handle::Create(); + g.tsf.AssociateFocus(const_cast(&s_tsfDataProvider)); + } // set the text area to have focus for accessibility consumers if (_pUiaProvider) @@ -855,7 +933,9 @@ void Window::_HandleWindowPosChanged(const LPARAM lParam) // - void Window::_HandleDrop(const WPARAM wParam) const { - Clipboard::Instance().PasteDrop((HDROP)wParam); + const auto drop = reinterpret_cast(wParam); + Clipboard::Instance().PasteDrop(drop); + DragFinish(drop); } [[nodiscard]] LRESULT Window::_HandleGetObject(const HWND hwnd, const WPARAM wParam, const LPARAM lParam) @@ -887,11 +967,6 @@ BOOL Window::PostUpdateWindowSize() const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto& ScreenInfo = GetScreenInfo(); - if (ScreenInfo.ConvScreenInfo != nullptr) - { - return FALSE; - } - if (gci.Flags & CONSOLE_SETTING_WINDOW_SIZE) { return FALSE; diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index f1e43bc98a..28295aeadd 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -18,6 +18,7 @@ // and thus may access both _r and _api. #pragma warning(disable : 4100) // '...': unreferenced formal parameter +#pragma warning(disable : 4127) // conditional expression is constant // Disable a bunch of warnings which get in the way of writing performant code. #pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23). #pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 2d6f4cc1a2..deb8925c94 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -56,6 +56,11 @@ Renderer::~Renderer() _pThread.reset(); } +IRenderData* Renderer::GetRenderData() const noexcept +{ + return _pData; +} + // Routine Description: // - Walks through the console data structures to compose a new frame based on the data that has changed since last call and outputs it to the connected rendering engine. // Arguments: @@ -118,24 +123,94 @@ Renderer::~Renderer() if (_currentCursorOptions) { - const auto coord = _currentCursorOptions->coordCursor; const auto& buffer = _pData->GetTextBuffer(); - const auto lineRendition = buffer.GetLineRendition(coord.y); - const auto cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1; + const auto view = buffer.GetSize(); + const auto coord = _currentCursorOptions->coordCursor; - til::rect cursorRect{ coord.x, coord.y, coord.x + cursorWidth, coord.y + 1 }; - cursorRect = BufferToScreenLine(cursorRect, lineRendition); - - if (buffer.GetSize().TrimToViewport(&cursorRect)) + // If we had previously drawn a composition at the previous cursor position + // we need to invalidate the entire line because who knows what changed. + // (It's possible to figure that out, but not worth the effort right now.) + if (_compositionCache) { - FOREACH_ENGINE(pEngine) + til::rect rect{ 0, coord.y, til::CoordTypeMax, coord.y + 1 }; + if (view.TrimToViewport(&rect)) { - LOG_IF_FAILED(pEngine->InvalidateCursor(&cursorRect)); + FOREACH_ENGINE(pEngine) + { + LOG_IF_FAILED(pEngine->Invalidate(&rect)); + } + } + } + else + { + const auto lineRendition = buffer.GetLineRendition(coord.y); + const auto cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1; + + til::rect rect{ coord.x, coord.y, coord.x + cursorWidth, coord.y + 1 }; + rect = BufferToScreenLine(rect, lineRendition); + + if (view.TrimToViewport(&rect)) + { + FOREACH_ENGINE(pEngine) + { + LOG_IF_FAILED(pEngine->InvalidateCursor(&rect)); + } } } } _currentCursorOptions = _GetCursorInfo(); + _compositionCache.reset(); + + // Invalidate the line that the active TSF composition is on, + // so that _PaintBufferOutput() actually gets a chance to draw it. + if (!_pData->activeComposition.text.empty()) + { + const auto viewport = _pData->GetViewport(); + const auto coordCursor = _pData->GetCursorPosition(); + + til::rect line{ 0, coordCursor.y, til::CoordTypeMax, coordCursor.y + 1 }; + if (viewport.TrimToViewport(&line)) + { + viewport.ConvertToOrigin(&line); + + FOREACH_ENGINE(pEngine) + { + LOG_IF_FAILED(pEngine->Invalidate(&line)); + } + + auto& buffer = _pData->GetTextBuffer(); + auto& scratch = buffer.GetScratchpadRow(); + + std::wstring_view text{ _pData->activeComposition.text }; + RowWriteState state{ + .columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(), + }; + + state.text = text.substr(0, _pData->activeComposition.cursorPos); + scratch.ReplaceText(state); + const auto cursorOffset = state.columnEnd; + + state.text = text.substr(_pData->activeComposition.cursorPos); + state.columnBegin = state.columnEnd; + scratch.ReplaceText(state); + + // Ideally the text is inserted at the position of the cursor (`coordCursor`), + // but if we got more text than fits into the remaining space until the end of the line, + // then we'll insert the text aligned to the end of the line. + const auto remaining = state.columnLimit - state.columnEnd; + const auto beg = std::clamp(coordCursor.x, 0, remaining); + + const auto baseAttribute = buffer.GetRowByOffset(coordCursor.y).GetAttrByColumn(coordCursor.x); + _compositionCache.emplace(til::point{ beg, coordCursor.y }, baseAttribute); + + // Fake-move the cursor to where it needs to be in the active composition. + if (_currentCursorOptions) + { + _currentCursorOptions->coordCursor.x = std::min(beg + cursorOffset, line.right - 1); + } + } + } FOREACH_ENGINE(pEngine) { @@ -195,9 +270,6 @@ try // 2. Paint Rows of Text _PaintBufferOutput(pEngine); - // 3. Paint overlays that reside above the text buffer - _PaintOverlays(pEngine); - // 4. Paint Selection _PaintSelection(pEngine); @@ -716,6 +788,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // It can move left/right or top/bottom depending on how the viewport is scrolled // relative to the entire buffer. const auto view = _pData->GetViewport(); + const auto compositionRow = _compositionCache ? _compositionCache->absoluteOrigin.y : -1; // This is effectively the number of cells on the visible screen that need to be redrawn. // The origin is always 0, 0 because it represents the screen itself, not the underlying buffer. @@ -746,7 +819,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) const auto redraw = Viewport::Intersect(dirty, view); // Retrieve the text buffer so we can read information out of it. - const auto& buffer = _pData->GetTextBuffer(); + auto& buffer = _pData->GetTextBuffer(); // Now walk through each row of text that we need to redraw. for (auto row = redraw.Top(); row < redraw.BottomExclusive(); row++) @@ -754,6 +827,53 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // Calculate the boundaries of a single line. This is from the left to right edge of the dirty // area in width and exactly 1 tall. const auto screenLine = til::inclusive_rect{ redraw.Left(), row, redraw.RightInclusive(), row }; + const auto& r = buffer.GetRowByOffset(row); + + // Draw the active composition. + // We have to use some tricks here with const_cast, because the code after it relies on TextBufferCellIterator, + // which isn't compatible with the scratchpad row. This forces us to back up and modify the actual row `r`. + ROW* rowBackup = nullptr; + if (row == compositionRow) + { + auto& scratch = buffer.GetScratchpadRow(); + scratch.CopyFrom(r); + rowBackup = &scratch; + + std::wstring_view text{ _pData->activeComposition.text }; + RowWriteState state{ + .columnLimit = r.GetReadableColumnCount(), + .columnEnd = _compositionCache->absoluteOrigin.x, + }; + + size_t off = 0; + for (const auto& range : _pData->activeComposition.attributes) + { + const auto len = range.len; + auto attr = range.attr; + + // Use the color at the cursor if TSF didn't specify any explicit color. + if (attr.GetBackground().IsDefault()) + { + attr.SetBackground(_compositionCache->baseAttribute.GetBackground()); + } + if (attr.GetForeground().IsDefault()) + { + attr.SetForeground(_compositionCache->baseAttribute.GetForeground()); + } + + state.text = text.substr(off, len); + state.columnBegin = state.columnEnd; + const_cast(r).ReplaceText(state); + const_cast(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr); + off += len; + } + } + const auto restore = wil::scope_exit([&] { + if (rowBackup) + { + const_cast(r).CopyFrom(*rowBackup); + } + }); // Convert the screen coordinates of the line to an equivalent // range of buffer cells, taking line rendition into account. @@ -1158,70 +1278,6 @@ void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine) return pEngine->PrepareRenderInfo(std::move(info)); } -// Routine Description: -// - Paint helper to draw text that overlays the main buffer to provide user interactivity regions -// - This supports IME composition. -// Arguments: -// - engine - The render engine that we're targeting. -// - overlay - The overlay to draw. -// Return Value: -// - -void Renderer::_PaintOverlay(IRenderEngine& engine, - const RenderOverlay& overlay) -{ - try - { - // Now get the overlay's viewport and adjust it to where it is supposed to be relative to the window. - auto srCaView = overlay.region.ToExclusive(); - srCaView.top += overlay.origin.y; - srCaView.bottom += overlay.origin.y; - srCaView.left += overlay.origin.x; - srCaView.right += overlay.origin.x; - - std::span dirtyAreas; - LOG_IF_FAILED(engine.GetDirtyArea(dirtyAreas)); - - for (const auto& rect : dirtyAreas) - { - if (const auto viewDirty = rect & srCaView) - { - for (auto iRow = viewDirty.top; iRow < viewDirty.bottom; iRow++) - { - const til::point target{ viewDirty.left, iRow }; - const auto source = target - overlay.origin; - - auto it = overlay.buffer.GetCellLineDataAt(source); - - _PaintBufferOutputHelper(&engine, it, target, false); - } - } - } - } - CATCH_LOG(); -} - -// Routine Description: -// - Paint helper to draw the composition string portion of the IME. -// - This specifically is the string that appears at the cursor on the input line showing what the user is currently typing. -// - See also: Generic Paint IME helper method. -// Arguments: -// - -// Return Value: -// - -void Renderer::_PaintOverlays(_In_ IRenderEngine* const pEngine) -{ - try - { - const auto overlays = _pData->GetOverlays(); - - for (const auto& overlay : overlays) - { - _PaintOverlay(*pEngine, overlay); - } - } - CATCH_LOG(); -} - // Routine Description: // - Paint helper to draw the selected area of the window. // Arguments: diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index a8184ce63d..1944beb4b9 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -44,6 +44,8 @@ namespace Microsoft::Console::Render ~Renderer(); + IRenderData* GetRenderData() const noexcept; + [[nodiscard]] HRESULT PaintFrame(); void NotifyPaintFrame() noexcept; @@ -93,6 +95,14 @@ namespace Microsoft::Console::Render void UpdateLastHoveredInterval(const std::optional::interval>& newInterval); private: + // Caches some essential information about the active composition. + // This allows us to properly invalidate it between frames, etc. + struct CompositionCache + { + til::point absoluteOrigin; + TextAttribute baseAttribute; + }; + static GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept; static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); @@ -106,8 +116,6 @@ namespace Microsoft::Console::Render bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept; void _PaintSelection(_In_ IRenderEngine* const pEngine); void _PaintCursor(_In_ IRenderEngine* const pEngine); - 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 usingSoftFont, const bool isSettingDefaultBrushes); [[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); std::vector _GetSelectionRects() const; @@ -127,6 +135,7 @@ namespace Microsoft::Console::Render std::optional::interval> _hoveredInterval; Microsoft::Console::Types::Viewport _viewport; std::optional _currentCursorOptions; + std::optional _compositionCache; std::vector _clusterBuffer; std::vector _previousSelection; std::function _pfnBackgroundColorChanged; diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index f9c08c83b9..e55bdcbe87 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -14,26 +14,26 @@ Author(s): #pragma once -#include "../../host/conimeinfo.h" #include "../../buffer/out/TextAttribute.hpp" +#include "../../renderer/inc/FontInfo.hpp" +#include "../../types/inc/viewport.hpp" class Cursor; +class TextBuffer; namespace Microsoft::Console::Render { - struct RenderOverlay final + struct CompositionRange { - // This is where the data is stored - const TextBuffer& buffer; + size_t len; // The number of chars in Composition::text that this .attr applies to + TextAttribute attr; + }; - // This is where the top left of the stored buffer should be overlaid on the screen - // (relative to the current visible viewport) - const til::point origin; - - // This is the area of the buffer that is actually used for overlay. - // Anything outside of this is considered empty by the overlay and shouldn't be used - // for painting purposes. - const Microsoft::Console::Types::Viewport region; + struct Composition + { + std::wstring text; + til::small_vector attributes; + size_t cursorPos = 0; }; class IRenderData @@ -44,7 +44,7 @@ namespace Microsoft::Console::Render // This block used to be IBaseData. virtual Microsoft::Console::Types::Viewport GetViewport() noexcept = 0; virtual til::point GetTextBufferEndPosition() const noexcept = 0; - virtual const TextBuffer& GetTextBuffer() const noexcept = 0; + virtual TextBuffer& GetTextBuffer() const noexcept = 0; virtual const FontInfo& GetFontInfo() const noexcept = 0; virtual std::vector GetSelectionRects() noexcept = 0; virtual std::span GetSearchHighlights() const noexcept = 0; @@ -60,7 +60,6 @@ namespace Microsoft::Console::Render virtual CursorType GetCursorStyle() const noexcept = 0; virtual ULONG GetCursorPixelWidth() const noexcept = 0; virtual bool IsCursorDoubleWidth() const = 0; - virtual const std::vector GetOverlays() const noexcept = 0; virtual const bool IsGridLineDrawingAllowed() noexcept = 0; virtual const std::wstring_view GetConsoleTitle() const noexcept = 0; virtual const std::wstring GetHyperlinkUri(uint16_t id) const = 0; @@ -76,5 +75,10 @@ namespace Microsoft::Console::Render virtual const til::point GetSelectionAnchor() const noexcept = 0; virtual const til::point GetSelectionEnd() const noexcept = 0; virtual const bool IsUiaDataInitialized() const noexcept = 0; + + // Ideally this would not be stored on an interface, however ideally IRenderData should not be an interface in the first place. + // This is because we should have only 1 way how to represent render data across the codebase anyway, and it should + // be by-value in a struct so that we can snapshot it and release the terminal lock as quickly as possible. + Composition activeComposition; }; } diff --git a/src/tsf/ConsoleTSF.cpp b/src/tsf/ConsoleTSF.cpp deleted file mode 100644 index c3e1b04b33..0000000000 --- a/src/tsf/ConsoleTSF.cpp +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "TfConvArea.h" -#include "TfEditSession.h" - -/* 626761ad-78d2-44d2-be8b-752cf122acec */ -const GUID GUID_APPLICATION = { 0x626761ad, 0x78d2, 0x44d2, { 0xbe, 0x8b, 0x75, 0x2c, 0xf1, 0x22, 0xac, 0xec } }; - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::Initialize -// -//---------------------------------------------------------------------------- - -#define Init_CheckResult() \ - if (FAILED(hr)) \ - { \ - Uninitialize(); \ - return hr; \ - } - -[[nodiscard]] HRESULT CConsoleTSF::Initialize() -{ - HRESULT hr; - - if (_spITfThreadMgr) - { - return S_FALSE; - } - - // Activate per-thread Cicero in custom UI mode (TF_TMAE_UIELEMENTENABLEDONLY). - - hr = ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - Init_CheckResult(); - _fCoInitialized = TRUE; - - hr = ::CoCreateInstance(CLSID_TF_ThreadMgr, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&_spITfThreadMgr)); - Init_CheckResult(); - - hr = _spITfThreadMgr->ActivateEx(&_tid, TF_TMAE_CONSOLE); - Init_CheckResult(); - - // Create Cicero document manager and input context. - - hr = _spITfThreadMgr->CreateDocumentMgr(&_spITfDocumentMgr); - Init_CheckResult(); - - TfEditCookie ecTmp; - hr = _spITfDocumentMgr->CreateContext(_tid, - 0, - static_cast(this), - &_spITfInputContext, - &ecTmp); - Init_CheckResult(); - - // Set the context owner before attaching the context to the doc. - wil::com_ptr_nothrow spSrcIC; - hr = _spITfInputContext.query_to(&spSrcIC); - Init_CheckResult(); - - hr = spSrcIC->AdviseSink(IID_ITfContextOwner, static_cast(this), &_dwContextOwnerCookie); - Init_CheckResult(); - - hr = _spITfDocumentMgr->Push(_spITfInputContext.get()); - Init_CheckResult(); - - // Collect the active keyboard layout info. - - wil::com_ptr_nothrow spITfProfilesMgr; - hr = ::CoCreateInstance(CLSID_TF_InputProcessorProfiles, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&spITfProfilesMgr)); - if (SUCCEEDED(hr)) - { - TF_INPUTPROCESSORPROFILE ipp; - hr = spITfProfilesMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &ipp); - if (SUCCEEDED(hr)) - { - OnActivated(ipp.dwProfileType, ipp.langid, ipp.clsid, ipp.catid, ipp.guidProfile, ipp.hkl, ipp.dwFlags); - } - } - Init_CheckResult(); - - // Setup some useful Cicero event sinks and callbacks. - // _spITfThreadMgr && _spITfInputContext must be non-null for checks above to have succeeded, so - // we're not going to check them again here. try_query will A/V if they're null. - auto spSrcTIM = _spITfThreadMgr.try_query(); - auto spSrcICS = _spITfInputContext.try_query(); - - hr = (spSrcTIM && spSrcIC && spSrcICS) ? S_OK : E_FAIL; - Init_CheckResult(); - - hr = spSrcTIM->AdviseSink(IID_ITfInputProcessorProfileActivationSink, - static_cast(this), - &_dwActivationSinkCookie); - Init_CheckResult(); - - hr = spSrcTIM->AdviseSink(IID_ITfUIElementSink, static_cast(this), &_dwUIElementSinkCookie); - Init_CheckResult(); - - hr = spSrcIC->AdviseSink(IID_ITfTextEditSink, static_cast(this), &_dwTextEditSinkCookie); - Init_CheckResult(); - - hr = spSrcICS->AdviseSingleSink(_tid, IID_ITfCleanupContextSink, static_cast(this)); - Init_CheckResult(); - - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::Uninitialize() -// -//---------------------------------------------------------------------------- - -void CConsoleTSF::Uninitialize() -{ - // Destroy the current conversion area object - - if (_pConversionArea) - { - delete _pConversionArea; - _pConversionArea = nullptr; - } - - // Detach Cicero event sinks. - if (_spITfInputContext) - { - auto spSrcICS = _spITfInputContext.try_query(); - if (spSrcICS) - { - spSrcICS->UnadviseSingleSink(_tid, IID_ITfCleanupContextSink); - } - } - - // Associate the document\context with the console window. - - if (_spITfThreadMgr) - { - auto spSrcTIM = _spITfThreadMgr.try_query(); - if (spSrcTIM) - { - if (_dwUIElementSinkCookie) - { - spSrcTIM->UnadviseSink(_dwUIElementSinkCookie); - } - if (_dwActivationSinkCookie) - { - spSrcTIM->UnadviseSink(_dwActivationSinkCookie); - } - } - } - - _dwUIElementSinkCookie = 0; - _dwActivationSinkCookie = 0; - - if (_spITfInputContext) - { - auto spSrcIC = _spITfInputContext.try_query(); - if (spSrcIC) - { - if (_dwContextOwnerCookie) - { - spSrcIC->UnadviseSink(_dwContextOwnerCookie); - } - if (_dwTextEditSinkCookie) - { - spSrcIC->UnadviseSink(_dwTextEditSinkCookie); - } - } - } - _dwContextOwnerCookie = 0; - _dwTextEditSinkCookie = 0; - - // Clear the Cicero reference to our document manager. - - if (_spITfThreadMgr && _spITfDocumentMgr) - { - wil::com_ptr_nothrow spDocMgr; - _spITfThreadMgr->AssociateFocus(_hwndConsole, nullptr, &spDocMgr); - } - - // Dismiss the input context and document manager. - - if (_spITfDocumentMgr) - { - _spITfDocumentMgr->Pop(TF_POPF_ALL); - } - - _spITfInputContext.reset(); - _spITfDocumentMgr.reset(); - - // Deactivate per-thread Cicero and uninitialize COM. - - if (_spITfThreadMgr) - { - _spITfThreadMgr->Deactivate(); - _spITfThreadMgr.reset(); - } - if (_fCoInitialized) - { - ::CoUninitialize(); - _fCoInitialized = FALSE; - } -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::IUnknown::QueryInterface -// CConsoleTSF::IUnknown::AddRef -// CConsoleTSF::IUnknown::Release -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::QueryInterface(REFIID riid, void** ppvObj) -{ - if (!ppvObj) - { - return E_FAIL; - } - *ppvObj = nullptr; - - if (IsEqualIID(riid, IID_ITfCleanupContextSink)) - { - *ppvObj = static_cast(this); - } - else if (IsEqualGUID(riid, IID_ITfContextOwnerCompositionSink)) - { - *ppvObj = static_cast(this); - } - else if (IsEqualIID(riid, IID_ITfUIElementSink)) - { - *ppvObj = static_cast(this); - } - else if (IsEqualIID(riid, IID_ITfContextOwner)) - { - *ppvObj = static_cast(this); - } - else if (IsEqualIID(riid, IID_ITfInputProcessorProfileActivationSink)) - { - *ppvObj = static_cast(this); - } - else if (IsEqualIID(riid, IID_ITfTextEditSink)) - { - *ppvObj = static_cast(this); - } - else if (IsEqualGUID(riid, IID_IUnknown)) - { - *ppvObj = this; - } - if (*ppvObj) - { - AddRef(); - } - return (*ppvObj) ? S_OK : E_NOINTERFACE; -} - -STDAPI_(ULONG) -CConsoleTSF::AddRef() -{ - return InterlockedIncrement(&_cRef); -} - -STDAPI_(ULONG) -CConsoleTSF::Release() -{ - auto cr = InterlockedDecrement(&_cRef); - if (cr == 0) - { - if (g_pConsoleTSF == this) - { - g_pConsoleTSF = nullptr; - } - delete this; - } - return cr; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::ITfCleanupContextSink::OnCleanupContext -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::OnCleanupContext(TfEditCookie ecWrite, ITfContext* pic) -{ - // - // Remove GUID_PROP_COMPOSING - // - wil::com_ptr_nothrow prop; - if (SUCCEEDED(pic->GetProperty(GUID_PROP_COMPOSING, &prop))) - { - wil::com_ptr_nothrow enumranges; - if (SUCCEEDED(prop->EnumRanges(ecWrite, &enumranges, nullptr))) - { - wil::com_ptr_nothrow rangeTmp; - while (enumranges->Next(1, &rangeTmp, nullptr) == S_OK) - { - VARIANT var; - VariantInit(&var); - prop->GetValue(ecWrite, rangeTmp.get(), &var); - if ((var.vt == VT_I4) && (var.lVal != 0)) - { - prop->Clear(ecWrite, rangeTmp.get()); - } - } - } - } - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::ITfContextOwnerCompositionSink::OnStartComposition -// CConsoleTSF::ITfContextOwnerCompositionSink::OnUpdateComposition -// CConsoleTSF::ITfContextOwnerCompositionSink::OnEndComposition -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::OnStartComposition(ITfCompositionView* pCompView, BOOL* pfOk) -{ - if (!_pConversionArea || (_cCompositions > 0 && (!_fModifyingDoc))) - { - *pfOk = FALSE; - } - else - { - *pfOk = TRUE; - // Ignore compositions triggered by our own edit sessions - // (i.e. when the application is the composition owner) - auto clsidCompositionOwner = GUID_APPLICATION; - pCompView->GetOwnerClsid(&clsidCompositionOwner); - if (!IsEqualGUID(clsidCompositionOwner, GUID_APPLICATION)) - { - _cCompositions++; - if (_cCompositions == 1) - { - LOG_IF_FAILED(ImeStartComposition()); - } - } - } - return S_OK; -} - -STDMETHODIMP CConsoleTSF::OnUpdateComposition(ITfCompositionView* /*pComp*/, ITfRange*) -{ - return S_OK; -} - -STDMETHODIMP CConsoleTSF::OnEndComposition(ITfCompositionView* pCompView) -{ - if (!_cCompositions || !_pConversionArea) - { - return E_FAIL; - } - // Ignore compositions triggered by our own edit sessions - // (i.e. when the application is the composition owner) - auto clsidCompositionOwner = GUID_APPLICATION; - pCompView->GetOwnerClsid(&clsidCompositionOwner); - if (!IsEqualGUID(clsidCompositionOwner, GUID_APPLICATION)) - { - _cCompositions--; - if (!_cCompositions) - { - LOG_IF_FAILED(_OnCompleteComposition()); - LOG_IF_FAILED(ImeEndComposition()); - } - } - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::ITfTextEditSink::OnEndEdit -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::OnEndEdit(ITfContext* pInputContext, TfEditCookie ecReadOnly, ITfEditRecord* pEditRecord) -{ - if (_cCompositions && _pConversionArea && _HasCompositionChanged(pInputContext, ecReadOnly, pEditRecord)) - { - LOG_IF_FAILED(_OnUpdateComposition()); - } - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::ITfInputProcessorProfileActivationSink::OnActivated -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::OnActivated(DWORD /*dwProfileType*/, - LANGID /*langid*/, - REFCLSID /*clsid*/, - REFGUID catid, - REFGUID /*guidProfile*/, - HKL /*hkl*/, - DWORD dwFlags) -{ - if (!(dwFlags & TF_IPSINK_FLAG_ACTIVE)) - { - return S_OK; - } - if (!IsEqualGUID(catid, GUID_TFCAT_TIP_KEYBOARD)) - { - // Don't care for non-keyboard profiles. - return S_OK; - } - - try - { - CreateConversionArea(); - } - CATCH_RETURN(); - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::ITfUIElementSink::BeginUIElement -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::BeginUIElement(DWORD /*dwUIElementId*/, BOOL* pbShow) -{ - *pbShow = TRUE; - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::ITfUIElementSink::UpdateUIElement -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::UpdateUIElement(DWORD /*dwUIElementId*/) -{ - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::ITfUIElementSink::EndUIElement -// -//---------------------------------------------------------------------------- - -STDMETHODIMP CConsoleTSF::EndUIElement(DWORD /*dwUIElementId*/) -{ - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::CreateConversionAreaService -// -//---------------------------------------------------------------------------- - -CConversionArea* CConsoleTSF::CreateConversionArea() -{ - BOOL fHadConvArea = (_pConversionArea != nullptr); - - if (!_pConversionArea) - { - _pConversionArea = new CConversionArea(); - } - - // Associate the document\context with the console window. - if (!fHadConvArea) - { - wil::com_ptr_nothrow spPrevDocMgr; - _spITfThreadMgr->AssociateFocus(_hwndConsole, _pConversionArea ? _spITfDocumentMgr.get() : nullptr, &spPrevDocMgr); - } - - return _pConversionArea; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::OnUpdateComposition() -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CConsoleTSF::_OnUpdateComposition() -{ - if (_fEditSessionRequested) - { - return S_FALSE; - } - - auto hr = E_OUTOFMEMORY; - auto pEditSession = new (std::nothrow) CEditSessionUpdateCompositionString(); - if (pEditSession) - { - // Can't use TF_ES_SYNC because called from OnEndEdit. - _fEditSessionRequested = TRUE; - _spITfInputContext->RequestEditSession(_tid, pEditSession, TF_ES_READWRITE, &hr); - if (FAILED(hr)) - { - pEditSession->Release(); - _fEditSessionRequested = FALSE; - } - } - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::OnCompleteComposition() -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CConsoleTSF::_OnCompleteComposition() -{ - // Update the composition area. - - auto hr = E_OUTOFMEMORY; - auto pEditSession = new (std::nothrow) CEditSessionCompositionComplete(); - if (pEditSession) - { - // The composition could have been finalized because of a caret move, therefore it must be - // inserted synchronously while at the original caret position.(TF_ES_SYNC is ok for a nested RO session). - _spITfInputContext->RequestEditSession(_tid, pEditSession, TF_ES_READ | TF_ES_SYNC, &hr); - if (FAILED(hr)) - { - pEditSession->Release(); - } - } - - // Cleanup (empty the context range) after the last composition, unless a new one has started. - if (!_fCleanupSessionRequested) - { - _fCleanupSessionRequested = TRUE; - auto pEditSessionCleanup = new (std::nothrow) CEditSessionCompositionCleanup(); - if (pEditSessionCleanup) - { - // Can't use TF_ES_SYNC because requesting RW while called within another session. - // For the same reason, must use explicit TF_ES_ASYNC, or the request will be rejected otherwise. - _spITfInputContext->RequestEditSession(_tid, pEditSessionCleanup, TF_ES_READWRITE | TF_ES_ASYNC, &hr); - if (FAILED(hr)) - { - pEditSessionCleanup->Release(); - _fCleanupSessionRequested = FALSE; - } - } - } - return hr; -} diff --git a/src/tsf/ConsoleTSF.h b/src/tsf/ConsoleTSF.h deleted file mode 100644 index 9a1b582bf2..0000000000 --- a/src/tsf/ConsoleTSF.h +++ /dev/null @@ -1,220 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfContext.h - -Abstract: - - This file defines the CConsoleTSF Interface Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#pragma once - -class CConversionArea; - -class CConsoleTSF final : - public ITfContextOwner, - public ITfContextOwnerCompositionSink, - public ITfInputProcessorProfileActivationSink, - public ITfUIElementSink, - public ITfCleanupContextSink, - public ITfTextEditSink -{ -public: - CConsoleTSF(HWND hwndConsole, - GetSuggestionWindowPos pfnPosition, - GetTextBoxAreaPos pfnTextArea) : - _hwndConsole(hwndConsole), - _pfnPosition(pfnPosition), - _pfnTextArea(pfnTextArea), - _cRef(1), - _tid() - { - } - - virtual ~CConsoleTSF() = default; - [[nodiscard]] HRESULT Initialize(); - void Uninitialize(); - -public: - // IUnknown methods - STDMETHODIMP QueryInterface(REFIID riid, void** ppvObj); - STDMETHODIMP_(ULONG) - AddRef(void); - STDMETHODIMP_(ULONG) - Release(void); - - // ITfContextOwner - STDMETHODIMP GetACPFromPoint(const POINT*, DWORD, LONG* pCP) - { - if (pCP) - { - *pCP = 0; - } - - return S_OK; - } - - // This returns Rectangle of the text box of whole console. - // When a user taps inside the rectangle while hardware keyboard is not available, - // touch keyboard is invoked. - STDMETHODIMP GetScreenExt(RECT* pRect) - { - if (pRect) - { - *pRect = _pfnTextArea(); - } - - return S_OK; - } - - // This returns rectangle of current command line edit area. - // When a user types in East Asian language, candidate window is shown at this position. - // Emoji and more panel (Win+.) is shown at the position, too. - STDMETHODIMP GetTextExt(LONG, LONG, RECT* pRect, BOOL* pbClipped) - { - if (pRect) - { - *pRect = _pfnPosition(); - } - - if (pbClipped) - { - *pbClipped = FALSE; - } - - return S_OK; - } - - STDMETHODIMP GetStatus(TF_STATUS* pTfStatus) - { - if (pTfStatus) - { - pTfStatus->dwDynamicFlags = 0; - pTfStatus->dwStaticFlags = TF_SS_TRANSITORY; - } - return pTfStatus ? S_OK : E_INVALIDARG; - } - STDMETHODIMP GetWnd(HWND* phwnd) - { - *phwnd = _hwndConsole; - return S_OK; - } - STDMETHODIMP GetAttribute(REFGUID, VARIANT*) - { - return E_NOTIMPL; - } - - // ITfContextOwnerCompositionSink methods - STDMETHODIMP OnStartComposition(ITfCompositionView* pComposition, BOOL* pfOk); - STDMETHODIMP OnUpdateComposition(ITfCompositionView* pComposition, ITfRange* pRangeNew); - STDMETHODIMP OnEndComposition(ITfCompositionView* pComposition); - - // ITfInputProcessorProfileActivationSink - STDMETHODIMP OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID clsid, REFGUID catid, REFGUID guidProfile, HKL hkl, DWORD dwFlags); - - // ITfUIElementSink methods - STDMETHODIMP BeginUIElement(DWORD dwUIElementId, BOOL* pbShow); - STDMETHODIMP UpdateUIElement(DWORD dwUIElementId); - STDMETHODIMP EndUIElement(DWORD dwUIElementId); - - // ITfCleanupContextSink methods - STDMETHODIMP OnCleanupContext(TfEditCookie ecWrite, ITfContext* pic); - - // ITfTextEditSink methods - STDMETHODIMP OnEndEdit(ITfContext* pInputContext, TfEditCookie ecReadOnly, ITfEditRecord* pEditRecord); - -public: - CConversionArea* CreateConversionArea(); - CConversionArea* GetConversionArea() { return _pConversionArea; } - ITfContext* GetInputContext() { return _spITfInputContext.get(); } - HWND GetConsoleHwnd() { return _hwndConsole; } - TfClientId GetTfClientId() { return _tid; } - BOOL IsInComposition() { return (_cCompositions > 0); } - void OnEditSession() { _fEditSessionRequested = FALSE; } - BOOL IsPendingCompositionCleanup() { return _fCleanupSessionRequested || _fCompositionCleanupSkipped; } - void OnCompositionCleanup(BOOL bSucceeded) - { - _fCleanupSessionRequested = FALSE; - _fCompositionCleanupSkipped = !bSucceeded; - } - void SetModifyingDocFlag(BOOL fSet) { _fModifyingDoc = fSet; } - void SetFocus(BOOL fSet) - { - if (!fSet && _cCompositions) - { - // Close (terminate) any open compositions when losing the input focus. - if (_spITfInputContext) - { - auto spCompositionServices = _spITfInputContext.try_query(); - if (spCompositionServices) - { - spCompositionServices->TerminateComposition(nullptr); - } - } - } - } - - // A workaround for a MS Korean IME scenario where the IME appends a whitespace - // composition programmatically right after completing a keyboard input composition. - // Since post-composition clean-up is an async operation, the programmatic whitespace - // composition gets completed before the previous composition cleanup happened, - // and this results in a double insertion of the first composition. To avoid that, we'll - // store the length of the last completed composition here until it's cleaned up. - // (for simplicity, this patch doesn't provide a generic solution for all possible - // scenarios with subsequent synchronous compositions, only for the known 'append'). - long GetCompletedRangeLength() const { return _cchCompleted; } - void SetCompletedRangeLength(long cch) { _cchCompleted = cch; } - -private: - [[nodiscard]] HRESULT _OnUpdateComposition(); - [[nodiscard]] HRESULT _OnCompleteComposition(); - BOOL _HasCompositionChanged(ITfContext* pInputContext, TfEditCookie ecReadOnly, ITfEditRecord* pEditRecord); - -private: - // ref count. - DWORD _cRef; - - // Cicero stuff. - TfClientId _tid; - wil::com_ptr_nothrow _spITfThreadMgr; - wil::com_ptr_nothrow _spITfDocumentMgr; - wil::com_ptr_nothrow _spITfInputContext; - - // Event sink cookies. - DWORD _dwContextOwnerCookie = 0; - DWORD _dwUIElementSinkCookie = 0; - DWORD _dwTextEditSinkCookie = 0; - DWORD _dwActivationSinkCookie = 0; - - // Conversion area object for the languages. - CConversionArea* _pConversionArea = nullptr; - - // Console info. - HWND _hwndConsole; - GetSuggestionWindowPos _pfnPosition; - GetTextBoxAreaPos _pfnTextArea; - - // Miscellaneous flags - BOOL _fModifyingDoc = FALSE; // Set TRUE, when calls ITfRange::SetText - BOOL _fCoInitialized = FALSE; - BOOL _fEditSessionRequested = FALSE; - BOOL _fCleanupSessionRequested = FALSE; - BOOL _fCompositionCleanupSkipped = FALSE; - - int _cCompositions = 0; - long _cchCompleted = 0; // length of completed composition waiting for cleanup -}; - -extern CConsoleTSF* g_pConsoleTSF; diff --git a/src/tsf/Handle.cpp b/src/tsf/Handle.cpp new file mode 100644 index 0000000000..ab3f7c5f78 --- /dev/null +++ b/src/tsf/Handle.cpp @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "Handle.h" + +#include "Implementation.h" + +using namespace Microsoft::Console::TSF; + +Handle Handle::Create() +{ + Handle handle; + handle._impl = new Implementation(); + handle._impl->Initialize(); + return handle; +} + +Handle::~Handle() +{ + if (_impl) + { + _impl->Uninitialize(); + _impl->Release(); + } +} + +Handle::Handle(Handle&& other) noexcept : + _impl{ other._impl } +{ + other._impl = nullptr; +} + +Handle& Handle::operator=(Handle&& other) noexcept +{ + if (this != &other) + { + this->~Handle(); + _impl = other._impl; + other._impl = nullptr; + } + return *this; +} + +Handle::operator bool() const noexcept +{ + return _impl != nullptr; +} + +HWND Handle::FindWindowOfActiveTSF() const noexcept +{ + return _impl ? _impl->FindWindowOfActiveTSF() : nullptr; +} + +void Handle::AssociateFocus(IDataProvider* provider) const +{ + if (_impl) + { + _impl->AssociateFocus(provider); + } +} + +void Handle::Focus(IDataProvider* provider) const +{ + if (_impl) + { + _impl->Focus(provider); + } +} + +void Handle::Unfocus(IDataProvider* provider) const +{ + if (_impl) + { + _impl->Unfocus(provider); + } +} + +bool Handle::HasActiveComposition() const noexcept +{ + return _impl ? _impl->HasActiveComposition() : false; +} diff --git a/src/tsf/Handle.h b/src/tsf/Handle.h new file mode 100644 index 0000000000..fc56920ae4 --- /dev/null +++ b/src/tsf/Handle.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace Microsoft::Console::Render +{ + class Renderer; +} + +namespace Microsoft::Console::TSF +{ + struct Implementation; + + // It is fine for any of the IDataProvider functions to throw. + // However, this doesn't apply to the IUnknown ones. + // + // NOTE: This is a pure virtual interface, just like those for COM. + // It cannot have a `~IDataProvider() = default;` destructor, because then it would need a vtable. + MIDL_INTERFACE("A86B8AAF-1531-40F5-95BB-611AA9DBDC18") + IDataProvider : IUnknown + { + virtual HWND GetHwnd() = 0; + virtual RECT GetViewport() = 0; + virtual RECT GetCursorPosition() = 0; + virtual void HandleOutput(std::wstring_view text) = 0; + virtual Render::Renderer* GetRenderer() = 0; + }; + + // A pimpl idiom wrapper for `Implementation` so that we don't pull in all the TSF headers everywhere. + // Simultaneously it allows us to handle AdviseSink/UnadviseSink properly, because those hold strong + // references on `Implementation` which results in an (unfortunate but intentional) reference cycle. + struct Handle + { + static Handle Create(); + + Handle() = default; + ~Handle(); + Handle(const Handle&) = delete; + Handle& operator=(const Handle&) = delete; + Handle(Handle&& other) noexcept; + Handle& operator=(Handle&& other) noexcept; + + explicit operator bool() const noexcept; + HWND FindWindowOfActiveTSF() const noexcept; + void AssociateFocus(IDataProvider* provider) const; + void Focus(IDataProvider* provider) const; + void Unfocus(IDataProvider* provider) const; + bool HasActiveComposition() const noexcept; + + private: + Implementation* _impl = nullptr; + }; +} diff --git a/src/tsf/Implementation.cpp b/src/tsf/Implementation.cpp new file mode 100644 index 0000000000..d3ad65ae9c --- /dev/null +++ b/src/tsf/Implementation.cpp @@ -0,0 +1,663 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "Implementation.h" + +#include "Handle.h" +#include "../buffer/out/TextAttribute.hpp" +#include "../renderer/base/renderer.hpp" + +#pragma warning(disable : 4100) // '...': unreferenced formal parameter + +using namespace Microsoft::Console::TSF; + +static void TfSelectionClose(const TF_SELECTION* sel) +{ + if (const auto r = sel->range) + { + r->Release(); + } +} +using unique_tf_selection = wil::unique_struct; + +static void TfPropertyvalClose(TF_PROPERTYVAL* val) +{ + VariantClear(&val->varValue); +} +using unique_tf_propertyval = wil::unique_struct; + +void Implementation::Initialize() +{ + _categoryMgr = wil::CoCreateInstance(CLSID_TF_CategoryMgr, CLSCTX_INPROC_SERVER); + _displayAttributeMgr = wil::CoCreateInstance(CLSID_TF_DisplayAttributeMgr); + + // There's no point in calling TF_GetThreadMgr. ITfThreadMgr is a per-thread singleton. + _threadMgrEx = wil::CoCreateInstance(CLSID_TF_ThreadMgr, CLSCTX_INPROC_SERVER); + + THROW_IF_FAILED(_threadMgrEx->ActivateEx(&_clientId, TF_TMAE_CONSOLE)); + THROW_IF_FAILED(_threadMgrEx->CreateDocumentMgr(_documentMgr.addressof())); + + TfEditCookie ecTextStore; + THROW_IF_FAILED(_documentMgr->CreateContext(_clientId, 0, static_cast(this), _context.addressof(), &ecTextStore)); + + _contextSource = _context.query(); + THROW_IF_FAILED(_contextSource->AdviseSink(IID_ITfContextOwner, static_cast(this), &_cookieContextOwner)); + THROW_IF_FAILED(_contextSource->AdviseSink(IID_ITfTextEditSink, static_cast(this), &_cookieTextEditSink)); + + THROW_IF_FAILED(_documentMgr->Push(_context.get())); +} + +void Implementation::Uninitialize() noexcept +{ + _provider.reset(); + + if (_associatedHwnd) + { + wil::com_ptr prev; + std::ignore = _threadMgrEx->AssociateFocus(_associatedHwnd, nullptr, prev.addressof()); + } + + if (_cookieTextEditSink != TF_INVALID_COOKIE) + { + std::ignore = _contextSource->UnadviseSink(_cookieTextEditSink); + } + if (_cookieContextOwner != TF_INVALID_COOKIE) + { + std::ignore = _contextSource->UnadviseSink(_cookieContextOwner); + } + + if (_documentMgr) + { + std::ignore = _documentMgr->Pop(TF_POPF_ALL); + } + if (_threadMgrEx) + { + std::ignore = _threadMgrEx->Deactivate(); + } +} + +HWND Implementation::FindWindowOfActiveTSF() const noexcept +{ + wil::com_ptr enumDocumentMgrs; + if (FAILED_LOG(_threadMgrEx->EnumDocumentMgrs(enumDocumentMgrs.addressof()))) + { + return nullptr; + } + + wil::com_ptr document; + if (FAILED_LOG(enumDocumentMgrs->Next(1, document.addressof(), nullptr))) + { + return nullptr; + } + + wil::com_ptr context; + if (FAILED_LOG(document->GetTop(context.addressof()))) + { + return nullptr; + } + + wil::com_ptr view; + if (FAILED_LOG(context->GetActiveView(view.addressof()))) + { + return nullptr; + } + + HWND hwnd; + if (FAILED_LOG(view->GetWnd(&hwnd))) + { + return nullptr; + } + + return hwnd; +} + +void Implementation::AssociateFocus(IDataProvider* provider) +{ + _provider = provider; + _associatedHwnd = _provider->GetHwnd(); + + wil::com_ptr prev; + THROW_IF_FAILED(_threadMgrEx->AssociateFocus(_associatedHwnd, _documentMgr.get(), prev.addressof())); +} + +void Implementation::Focus(IDataProvider* provider) +{ + _provider = provider; + + THROW_IF_FAILED(_threadMgrEx->SetFocus(_documentMgr.get())); +} + +void Implementation::Unfocus(IDataProvider* provider) +{ + if (!_provider || _provider != provider) + { + return; + } + + { + const auto renderer = _provider->GetRenderer(); + const auto renderData = renderer->GetRenderData(); + + renderData->LockConsole(); + const auto unlock = wil::scope_exit([&]() { + renderData->UnlockConsole(); + }); + + if (!renderData->activeComposition.text.empty()) + { + auto& comp = renderData->activeComposition; + comp.text.clear(); + comp.attributes.clear(); + renderer->NotifyPaintFrame(); + } + } + + _provider.reset(); + + if (_compositions > 0) + { + if (const auto svc = _context.try_query()) + { + svc->TerminateComposition(nullptr); + } + } +} + +bool Implementation::HasActiveComposition() const noexcept +{ + return _compositions > 0; +} + +#pragma region IUnknown + +STDMETHODIMP Implementation::QueryInterface(REFIID riid, void** ppvObj) noexcept +{ + if (!ppvObj) + { + return E_POINTER; + } + + if (IsEqualGUID(riid, IID_ITfContextOwner)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualGUID(riid, IID_ITfContextOwnerCompositionSink)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualGUID(riid, IID_ITfTextEditSink)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualGUID(riid, IID_IUnknown)) + { + *ppvObj = static_cast(static_cast(this)); + } + else + { + *ppvObj = nullptr; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +ULONG STDMETHODCALLTYPE Implementation::AddRef() noexcept +{ + return InterlockedIncrement(&_referenceCount); +} + +ULONG STDMETHODCALLTYPE Implementation::Release() noexcept +{ + const auto r = InterlockedDecrement(&_referenceCount); + if (r == 0) + { + delete this; + } + return r; +} + +#pragma endregion IUnknown + +#pragma region ITfContextOwner + +STDMETHODIMP Implementation::GetACPFromPoint(const POINT* ptScreen, DWORD dwFlags, LONG* pacp) noexcept +{ + assert(false); + return E_NOTIMPL; +} + +// This returns rectangle of current command line edit area. +// When a user types in East Asian language, candidate window is shown at this position. +// Emoji and more panel (Win+.) is shown at the position, too. +STDMETHODIMP Implementation::GetTextExt(LONG acpStart, LONG acpEnd, RECT* prc, BOOL* pfClipped) noexcept +try +{ + if (prc) + { + *prc = _provider ? _provider->GetCursorPosition() : RECT{}; + } + + if (pfClipped) + { + *pfClipped = FALSE; + } + + return S_OK; +} +CATCH_RETURN() + +// This returns Rectangle of the text box of whole console. +// When a user taps inside the rectangle while hardware keyboard is not available, touch keyboard is invoked. +STDMETHODIMP Implementation::GetScreenExt(RECT* prc) noexcept +try +{ + if (prc) + { + *prc = _provider ? _provider->GetViewport() : RECT{}; + } + + return S_OK; +} +CATCH_RETURN() + +STDMETHODIMP Implementation::GetStatus(TF_STATUS* pdcs) noexcept +{ + if (pdcs) + { + pdcs->dwDynamicFlags = 0; + // The use of TF_SS_TRANSITORY / TS_SS_TRANSITORY is incredibly important... + // ...and it has the least complete description: + // > TS_SS_TRANSITORY: The document is expected to have a short usage cycle. + // + // Proper documentation about the flag has been lost and can only be found via archive.org: + // http://web.archive.org/web/20140520210042/http://blogs.msdn.com/b/tsfaware/archive/2007/04/25/transitory-contexts.aspx + // It states: + // > The most significant difference is that Transitory contexts don't retain state - once you end the composition [...], + // > any knowledge of the document (or any previous insertions/modifications/etc.) is gone. + // In other words, non-transitory contexts expect access to previously completed contents, which is something we cannot provide. + // Because once some text has finished composition we'll immediately send it to the shell via HandleOutput(), which we cannot undo. + // It's also the primary reason why we cannot use the WinRT CoreTextServices APIs, as they don't set TS_SS_TRANSITORY. + // + // Additionally, "short usage cycle" also significantly undersells another importance of the flag: + // If set, it enables CUAS, the Cicero Unaware Application Support, which is an emulation layer that fakes IMM32. + // Cicero is the internal code name for TSF. In other words, "TS_SS_TRANSITORY" = "Disable modern TSF". + // This results in a couple modern composition features not working (Korean reconversion primarily), + // but it's a trade-off we're forced to make, because otherwise it doesn't work at all. + // + // TS_SS_NOHIDDENTEXT tells TSF that we don't support TS_RT_HIDDEN, which is used if a document contains hidden markup + // inside the text. For instance an HTML document contains tags which aren't visible, but nonetheless exist. + // It's not publicly documented, but allegedly specifying this flag results in a minor performance uplift. + // Ironically, the only two places that mention this flag internally state: + // > perf: we could check TS_SS_NOHIDDENTEXT for better perf + pdcs->dwStaticFlags = TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT; + } + + return S_OK; +} + +STDMETHODIMP Implementation::GetWnd(HWND* phwnd) noexcept +{ + *phwnd = _provider ? _provider->GetHwnd() : nullptr; + return S_OK; +} + +STDMETHODIMP Implementation::GetAttribute(REFGUID rguidAttribute, VARIANT* pvarValue) noexcept +{ + return E_NOTIMPL; +} + +#pragma endregion ITfContextOwner + +#pragma region ITfContextOwnerCompositionSink + +STDMETHODIMP Implementation::OnStartComposition(ITfCompositionView* pComposition, BOOL* pfOk) noexcept +try +{ + _compositions++; + *pfOk = TRUE; + return S_OK; +} +CATCH_RETURN() + +STDMETHODIMP Implementation::OnUpdateComposition(ITfCompositionView* pComposition, ITfRange* pRangeNew) noexcept +{ + return S_OK; +} + +STDMETHODIMP Implementation::OnEndComposition(ITfCompositionView* pComposition) noexcept +try +{ + if (_compositions <= 0) + { + return E_FAIL; + } + + _compositions--; + if (_compositions == 0) + { + // https://learn.microsoft.com/en-us/windows/win32/api/msctf/nf-msctf-itfcontext-requesteditsession + // > A text service can request an edit session within the context of an existing edit session, + // > provided a write access session is not requested within a read-only session. + // --> Requires TF_ES_ASYNC to work properly. TF_ES_ASYNCDONTCARE randomly fails because... TSF. + std::ignore = _request(_editSessionCompositionUpdate, TF_ES_READWRITE | TF_ES_ASYNC); + } + + return S_OK; +} +CATCH_RETURN() + +#pragma endregion ITfContextOwnerCompositionSink + +#pragma region ITfTextEditSink + +STDMETHODIMP Implementation::OnEndEdit(ITfContext* pic, TfEditCookie ecReadOnly, ITfEditRecord* pEditRecord) noexcept +try +{ + if (_compositions == 1) + { + // https://learn.microsoft.com/en-us/windows/win32/api/msctf/nf-msctf-itfcontext-requesteditsession + // > A text service can request an edit session within the context of an existing edit session, + // > provided a write access session is not requested within a read-only session. + // --> Requires TF_ES_ASYNC to work properly. TF_ES_ASYNCDONTCARE randomly fails because... TSF. + std::ignore = _request(_editSessionCompositionUpdate, TF_ES_READWRITE | TF_ES_ASYNC); + } + + return S_OK; +} +CATCH_RETURN() + +#pragma endregion ITfTextEditSink + +Implementation::EditSessionProxyBase::EditSessionProxyBase(Implementation* self) noexcept : + self{ self } +{ +} + +STDMETHODIMP Implementation::EditSessionProxyBase::QueryInterface(REFIID riid, void** ppvObj) noexcept +{ + if (!ppvObj) + { + return E_POINTER; + } + + if (IsEqualGUID(riid, IID_ITfEditSession)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualGUID(riid, IID_IUnknown)) + { + *ppvObj = static_cast(this); + } + else + { + *ppvObj = nullptr; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +ULONG STDMETHODCALLTYPE Implementation::EditSessionProxyBase::AddRef() noexcept +{ + return InterlockedIncrement(&referenceCount); +} + +ULONG STDMETHODCALLTYPE Implementation::EditSessionProxyBase::Release() noexcept +{ + FAIL_FAST_IF(referenceCount == 0); + return InterlockedDecrement(&referenceCount); +} + +[[nodiscard]] HRESULT Implementation::_request(EditSessionProxyBase& session, DWORD flags) const +{ + // Some of the sessions are async, and we don't want to send another request if one is still in flight. + if (session.referenceCount) + { + return S_FALSE; + } + + HRESULT hr = S_OK; + THROW_IF_FAILED(_context->RequestEditSession(_clientId, &session, flags, &hr)); + RETURN_IF_FAILED(hr); + return S_OK; +} + +void Implementation::_doCompositionUpdate(TfEditCookie ec) +{ + wil::com_ptr fullRange; + LONG fullRangeLength; + THROW_IF_FAILED(_context->GetStart(ec, fullRange.addressof())); + THROW_IF_FAILED(fullRange->ShiftEnd(ec, LONG_MAX, &fullRangeLength, nullptr)); + + std::wstring finalizedString; + std::wstring activeComposition; + til::small_vector activeCompositionRanges; + bool firstRange = true; + + const GUID* guids[] = { &GUID_PROP_COMPOSING, &GUID_PROP_ATTRIBUTE }; + wil::com_ptr props; + THROW_IF_FAILED(_context->TrackProperties(&guids[0], ARRAYSIZE(guids), nullptr, 0, props.addressof())); + + wil::com_ptr enumRanges; + THROW_IF_FAILED(props->EnumRanges(ec, enumRanges.addressof(), fullRange.get())); + + // IEnumTfRanges::Next returns S_FALSE when it has reached the end of the list. + // This includes any call where the number of returned items is less than what was requested. + for (HRESULT nextResult = S_OK; nextResult == S_OK;) + { + ITfRange* ranges[8]; + ULONG rangesCount; + nextResult = enumRanges->Next(ARRAYSIZE(ranges), &ranges[0], &rangesCount); + + const auto cleanup = wil::scope_exit([&] { + for (ULONG i = 0; i < rangesCount; ++i) + { + ranges[i]->Release(); + } + }); + + for (ULONG i = 0; i < rangesCount; ++i) + { + const auto range = ranges[i]; + + bool composing = false; + TfGuidAtom atom = TF_INVALID_GUIDATOM; + { + wil::unique_variant var; + THROW_IF_FAILED(props->GetValue(ec, range, var.addressof())); + + wil::com_ptr propVal; + wil::com_query_to(var.punkVal, propVal.addressof()); + + unique_tf_propertyval propVals[2]; + THROW_IF_FAILED(propVal->Next(2, propVals[0].addressof(), nullptr)); + + for (const auto& val : propVals) + { + if (IsEqualGUID(val.guidId, GUID_PROP_COMPOSING)) + { + composing = V_VT(&val.varValue) == VT_I4 && V_I4(&val.varValue) != 0; + } + else if (IsEqualGUID(val.guidId, GUID_PROP_ATTRIBUTE)) + { + atom = V_VT(&val.varValue) == VT_I4 ? static_cast(V_I4(&val.varValue)) : TF_INVALID_GUIDATOM; + } + } + } + + size_t totalLen = 0; + for (;;) + { + // GetText() won't throw if the range is empty. It'll simply return len == 0. + // However, you'll likely never see this happen with a bufCap this large (try 16 instead or something). + // It seems TSF doesn't support such large compositions in any language. + static constexpr ULONG bufCap = 128; + WCHAR buf[bufCap]; + ULONG len = bufCap; + THROW_IF_FAILED(range->GetText(ec, TF_TF_MOVESTART, buf, len, &len)); + + if (!composing && firstRange) + { + finalizedString.append(buf, len); + } + else + { + activeComposition.append(buf, len); + } + + totalLen += len; + + if (len < bufCap) + { + break; + } + } + + const auto attr = _textAttributeFromAtom(atom); + activeCompositionRanges.emplace_back(totalLen, attr); + + firstRange = false; + } + } + + LONG cursorPos = LONG_MAX; + { + // According to the docs this may result in TF_E_NOSELECTION. While I haven't actually seen that happen myself yet, + // I don't want this to result in log-spam, which is why this doesn't use SUCCEEDED_LOG(). + unique_tf_selection sel; + ULONG selCount; + if (SUCCEEDED(_context->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &sel, &selCount)) && selCount == 1) + { + wil::com_ptr start; + THROW_IF_FAILED(_context->GetStart(ec, start.addressof())); + + TF_HALTCOND hc{ + .pHaltRange = sel.range, + .aHaltPos = sel.style.ase == TF_AE_START ? TF_ANCHOR_START : TF_ANCHOR_END, + }; + THROW_IF_FAILED(start->ShiftEnd(ec, LONG_MAX, &cursorPos, &hc)); + } + + // Compensate for the fact that we'll be erasing the start of the string below. + cursorPos -= static_cast(finalizedString.size()); + cursorPos = std::clamp(cursorPos, 0l, static_cast(activeComposition.size())); + } + + if (!finalizedString.empty()) + { + // Erase the text that's done with composition from the context. + wil::com_ptr range; + LONG cch; + THROW_IF_FAILED(_context->GetStart(ec, range.addressof())); + THROW_IF_FAILED(range->ShiftEnd(ec, static_cast(finalizedString.size()), &cch, nullptr)); + THROW_IF_FAILED(range->SetText(ec, 0, nullptr, 0)); + } + + if (_provider) + { + { + const auto renderer = _provider->GetRenderer(); + const auto renderData = renderer->GetRenderData(); + + renderData->LockConsole(); + const auto unlock = wil::scope_exit([&]() { + renderData->UnlockConsole(); + }); + + auto& comp = renderData->activeComposition; + comp.text = std::move(activeComposition); + comp.attributes = std::move(activeCompositionRanges); + // The code block above that calculates the `cursorPos` will clamp it to a positive number. + comp.cursorPos = static_cast(cursorPos); + renderer->NotifyPaintFrame(); + } + + if (!finalizedString.empty()) + { + _provider->HandleOutput(finalizedString); + } + } +} + +TextAttribute Implementation::_textAttributeFromAtom(TfGuidAtom atom) const +{ + TextAttribute attr; + + // You get TF_INVALID_GUIDATOM by (for instance) using the Vietnamese Telex IME. + // A dashed underline is used because that's what Firefox used at the time and it + // looked kind of neat. In the past, conhost used a blue background and white text. + if (atom == TF_INVALID_GUIDATOM) + { + attr.SetUnderlineStyle(UnderlineStyle::DashedUnderlined); + return attr; + } + + GUID guid; + if (FAILED_LOG(_categoryMgr->GetGUID(atom, &guid))) + { + return attr; + } + + wil::com_ptr dai; + if (FAILED_LOG(_displayAttributeMgr->GetDisplayAttributeInfo(guid, dai.addressof(), nullptr))) + { + return attr; + } + + TF_DISPLAYATTRIBUTE da; + THROW_IF_FAILED(dai->GetAttributeInfo(&da)); + + if (da.crText.type != TF_CT_NONE) + { + attr.SetForeground(_colorFromDisplayAttribute(da.crText)); + } + if (da.crBk.type != TF_CT_NONE) + { + attr.SetBackground(_colorFromDisplayAttribute(da.crBk)); + } + if (da.lsStyle >= TF_LS_NONE && da.lsStyle <= TF_LS_SQUIGGLE) + { + static constexpr UnderlineStyle lut[] = { + /* TF_LS_NONE */ UnderlineStyle::NoUnderline, + /* TF_LS_SOLID */ UnderlineStyle::SinglyUnderlined, + /* TF_LS_DOT */ UnderlineStyle::DottedUnderlined, + /* TF_LS_DASH */ UnderlineStyle::DashedUnderlined, + /* TF_LS_SQUIGGLE */ UnderlineStyle::CurlyUnderlined, + }; + attr.SetUnderlineStyle(lut[da.lsStyle]); + } + // You can reproduce bold lines with the Japanese IME by typing "kyouhaishaheiku" and pressing space. + // The IME will allow you to navigate between the 3 parts of the composition and the current one is + // marked as fBoldLine. We don't support bold lines so we just use a double underline instead. + if (da.fBoldLine) + { + attr.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); + } + if (da.crLine.type != TF_CT_NONE) + { + attr.SetUnderlineColor(_colorFromDisplayAttribute(da.crLine)); + } + + return attr; +} + +COLORREF Implementation::_colorFromDisplayAttribute(TF_DA_COLOR color) +{ + switch (color.type) + { + case TF_CT_SYSCOLOR: + return GetSysColor(color.nIndex); + case TF_CT_COLORREF: + return color.cr; + default: + // If you get here you either called this when .type is TF_CT_NONE + // (don't call in that case; there's no color to be had), or + // there's a new .type which you need to add. + assert(false); + return 0; + } +} diff --git a/src/tsf/Implementation.h b/src/tsf/Implementation.h new file mode 100644 index 0000000000..7071141cae --- /dev/null +++ b/src/tsf/Implementation.h @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +class TextAttribute; + +namespace Microsoft::Console::Render +{ + class Renderer; +} + +namespace Microsoft::Console::TSF +{ + struct IDataProvider; + + struct Implementation : ITfContextOwner, ITfContextOwnerCompositionSink, ITfTextEditSink + { + virtual ~Implementation() = default; + + void Initialize(); + void Uninitialize() noexcept; + HWND FindWindowOfActiveTSF() const noexcept; + void AssociateFocus(IDataProvider* provider); + void Focus(IDataProvider* provider); + void Unfocus(IDataProvider* provider); + bool HasActiveComposition() const noexcept; + + // IUnknown methods + STDMETHODIMP QueryInterface(REFIID riid, void** ppvObj) noexcept override; + ULONG STDMETHODCALLTYPE AddRef() noexcept override; + ULONG STDMETHODCALLTYPE Release() noexcept override; + + // ITfContextOwner + STDMETHODIMP GetACPFromPoint(const POINT* ptScreen, DWORD dwFlags, LONG* pacp) noexcept override; + STDMETHODIMP GetTextExt(LONG acpStart, LONG acpEnd, RECT* prc, BOOL* pfClipped) noexcept override; + STDMETHODIMP GetScreenExt(RECT* prc) noexcept override; + STDMETHODIMP GetStatus(TF_STATUS* pdcs) noexcept override; + STDMETHODIMP GetWnd(HWND* phwnd) noexcept override; + STDMETHODIMP GetAttribute(REFGUID rguidAttribute, VARIANT* pvarValue) noexcept override; + + // ITfContextOwnerCompositionSink methods + STDMETHODIMP OnStartComposition(ITfCompositionView* pComposition, BOOL* pfOk) noexcept override; + STDMETHODIMP OnUpdateComposition(ITfCompositionView* pComposition, ITfRange* pRangeNew) noexcept override; + STDMETHODIMP OnEndComposition(ITfCompositionView* pComposition) noexcept override; + + // ITfTextEditSink methods + STDMETHODIMP OnEndEdit(ITfContext* pic, TfEditCookie ecReadOnly, ITfEditRecord* pEditRecord) noexcept override; + + private: + struct EditSessionProxyBase : ITfEditSession + { + explicit EditSessionProxyBase(Implementation* self) noexcept; + virtual ~EditSessionProxyBase() = default; + + // IUnknown methods + STDMETHODIMP QueryInterface(REFIID riid, void** ppvObj) noexcept override; + ULONG STDMETHODCALLTYPE AddRef() noexcept override; + ULONG STDMETHODCALLTYPE Release() noexcept override; + + ULONG referenceCount = 0; + Implementation* self = nullptr; + }; + + // In the past we had 3 different `ITfEditSession`s (update, finish, cleanup). + // Due to refactoring only 1 is left now, but this code remains in case we need more in the future. + // It allows you to statically bind a callback function to a `ITfEditSession` proxy type. + template + struct EditSessionProxy : EditSessionProxyBase + { + using EditSessionProxyBase::EditSessionProxyBase; + + // ITfEditSession method + STDMETHODIMP DoEditSession(TfEditCookie ec) noexcept override + { + try + { + (self->*Callback)(ec); + return S_OK; + } + CATCH_RETURN(); + } + }; + + [[nodiscard]] HRESULT _request(EditSessionProxyBase& session, DWORD flags) const; + void _doCompositionUpdate(TfEditCookie ec); + TextAttribute _textAttributeFromAtom(TfGuidAtom atom) const; + static COLORREF _colorFromDisplayAttribute(TF_DA_COLOR color); + + ULONG _referenceCount = 1; + + wil::com_ptr _provider; + HWND _associatedHwnd = nullptr; + + wil::com_ptr_t _categoryMgr; + wil::com_ptr _displayAttributeMgr; + wil::com_ptr _threadMgrEx; + wil::com_ptr _documentMgr; + wil::com_ptr _context; + wil::com_ptr _contextSource; + DWORD _cookieContextOwner = TF_INVALID_COOKIE; + DWORD _cookieTextEditSink = TF_INVALID_COOKIE; + TfClientId _clientId = TF_CLIENTID_NULL; + + EditSessionProxy<&Implementation::_doCompositionUpdate> _editSessionCompositionUpdate{ this }; + int _compositions = 0; + }; +} diff --git a/src/tsf/TfCatUtil.cpp b/src/tsf/TfCatUtil.cpp deleted file mode 100644 index 2ded71dd88..0000000000 --- a/src/tsf/TfCatUtil.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfCatUtil.cpp - -Abstract: - - This file implements the CicCategoryMgr Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#include "precomp.h" -#include "TfCatUtil.h" - -//+--------------------------------------------------------------------------- -// -// CicCategoryMgr::ctor -// CicCategoryMgr::dtor -// -//---------------------------------------------------------------------------- - -CicCategoryMgr::CicCategoryMgr() = default; - -CicCategoryMgr::~CicCategoryMgr() = default; - -//+--------------------------------------------------------------------------- -// -// CicCategoryMgr::GetGUIDFromGUIDATOM -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CicCategoryMgr::GetGUIDFromGUIDATOM(TfGuidAtom guidatom, GUID* pguid) -{ - return m_pcat->GetGUID(guidatom, pguid); -} - -//+--------------------------------------------------------------------------- -// -// CicCategoryMgr::InitCategoryInstance -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CicCategoryMgr::InitCategoryInstance() -{ - // - // Create ITfCategoryMgr instance. - // - return ::CoCreateInstance(CLSID_TF_CategoryMgr, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&m_pcat)); -} diff --git a/src/tsf/TfCatUtil.h b/src/tsf/TfCatUtil.h deleted file mode 100644 index c749098dd5..0000000000 --- a/src/tsf/TfCatUtil.h +++ /dev/null @@ -1,38 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfCatUtil.h - -Abstract: - - This file defines the CicCategoryMgr Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#pragma once - -class CicCategoryMgr -{ -public: - CicCategoryMgr(); - virtual ~CicCategoryMgr(); - -public: - [[nodiscard]] HRESULT GetGUIDFromGUIDATOM(TfGuidAtom guidatom, GUID* pguid); - [[nodiscard]] HRESULT InitCategoryInstance(); - - inline ITfCategoryMgr* GetCategoryMgr() { return m_pcat.get(); } - -private: - wil::com_ptr_nothrow m_pcat; -}; diff --git a/src/tsf/TfConvArea.cpp b/src/tsf/TfConvArea.cpp deleted file mode 100644 index 940bb7af09..0000000000 --- a/src/tsf/TfConvArea.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfConvArea.cpp - -Abstract: - - This file implements the CConversionArea Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#include "precomp.h" -#include "ConsoleTSF.h" -#include "TfCtxtComp.h" -#include "TfConvArea.h" - -//+--------------------------------------------------------------------------- -// CConversionArea -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CConversionArea::DrawComposition(const std::wstring_view CompStr, - const std::vector& DisplayAttributes, - const DWORD CompCursorPos) -{ - // Set up colors. - static const std::array colors{ DEFAULT_COMP_ENTERED, - DEFAULT_COMP_ALREADY_CONVERTED, - DEFAULT_COMP_CONVERSION, - DEFAULT_COMP_YET_CONVERTED, - DEFAULT_COMP_INPUT_ERROR, - DEFAULT_COMP_INPUT_ERROR, - DEFAULT_COMP_INPUT_ERROR, - DEFAULT_COMP_INPUT_ERROR }; - - const auto encodedAttributes = _DisplayAttributesToEncodedAttributes(DisplayAttributes, - CompCursorPos); - - std::span attributes(encodedAttributes.data(), encodedAttributes.size()); - std::span colorArray(colors.data(), colors.size()); - - return ImeComposeData(CompStr, attributes, colorArray); -} - -[[nodiscard]] HRESULT CConversionArea::ClearComposition() -{ - return ImeClearComposeData(); -} - -[[nodiscard]] HRESULT CConversionArea::DrawResult(const std::wstring_view ResultStr) -{ - return ImeComposeResult(ResultStr); -} - -[[nodiscard]] std::vector CConversionArea::_DisplayAttributesToEncodedAttributes(const std::vector& DisplayAttributes, - const DWORD CompCursorPos) -{ - std::vector encodedAttrs; - for (const auto& da : DisplayAttributes) - { - BYTE bAttr; - - if (da.bAttr == TF_ATTR_OTHER || da.bAttr > TF_ATTR_FIXEDCONVERTED) - { - bAttr = ATTR_TARGET_CONVERTED; - } - else - { - if (da.bAttr == TF_ATTR_INPUT_ERROR) - { - bAttr = ATTR_CONVERTED; - } - else - { - bAttr = (BYTE)da.bAttr; - } - } - encodedAttrs.emplace_back(bAttr); - } - - if (CompCursorPos != -1) - { - if (CompCursorPos == 0) - { - encodedAttrs[CompCursorPos] |= CONIME_CURSOR_LEFT; // special handling for ConSrv... 0x20 = COMMON_LVB_GRID_SINGLEFLAG + COMMON_LVB_GRID_LVERTICAL - } - else if (CompCursorPos - 1 < DisplayAttributes.size()) - { - encodedAttrs[CompCursorPos - 1] |= CONIME_CURSOR_RIGHT; // special handling for ConSrv... 0x10 = COMMON_LVB_GRID_SINGLEFLAG + COMMON_LVB_GRID_RVERTICAL - } - } - - return encodedAttrs; -} diff --git a/src/tsf/TfConvArea.h b/src/tsf/TfConvArea.h deleted file mode 100644 index e6ae9466ed..0000000000 --- a/src/tsf/TfConvArea.h +++ /dev/null @@ -1,44 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfConvArea.h - -Abstract: - - This file defines the CConversionAreaJapanese Interface Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#pragma once - -//+--------------------------------------------------------------------------- -// -// CConversionArea::Pure virtual class -// -//---------------------------------------------------------------------------- - -class CConversionArea -{ -public: - [[nodiscard]] HRESULT DrawComposition(const std::wstring_view CompStr, - const std::vector& DisplayAttributes, - const DWORD CompCursorPos = -1); - - [[nodiscard]] HRESULT ClearComposition(); - - [[nodiscard]] HRESULT DrawResult(const std::wstring_view ResultStr); - -private: - [[nodiscard]] std::vector _DisplayAttributesToEncodedAttributes(const std::vector& DisplayAttributes, - const DWORD CompCursorPos); -}; diff --git a/src/tsf/TfCtxtComp.h b/src/tsf/TfCtxtComp.h deleted file mode 100644 index 5b5ef46031..0000000000 --- a/src/tsf/TfCtxtComp.h +++ /dev/null @@ -1,44 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfCtxtComp.h - -Abstract: - - This file defines the Context of Composition Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#pragma once - -///////////////////////////////////////////////////////////////////////////// -// CCompCursorPos - -class CCompCursorPos -{ -public: - CCompCursorPos() - { - m_CursorPosition = 0; - } - - void SetCursorPosition(DWORD CursorPosition) - { - m_CursorPosition = CursorPosition; - } - - DWORD GetCursorPosition() { return m_CursorPosition; } - -private: - DWORD m_CursorPosition; -}; diff --git a/src/tsf/TfDispAttr.cpp b/src/tsf/TfDispAttr.cpp deleted file mode 100644 index 48e1d044e8..0000000000 --- a/src/tsf/TfDispAttr.cpp +++ /dev/null @@ -1,199 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfDispAttr.cpp - -Abstract: - - This file implements the CicDisplayAttributeMgr Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#include "precomp.h" -#include "TfDispAttr.h" - -//+--------------------------------------------------------------------------- -// -// CicDisplayAttributeMgr::ctor -// CicDisplayAttributeMgr::dtor -// -//---------------------------------------------------------------------------- - -CicDisplayAttributeMgr::CicDisplayAttributeMgr() = default; - -CicDisplayAttributeMgr::~CicDisplayAttributeMgr() = default; - -//+--------------------------------------------------------------------------- -// -// CicDisplayAttributeMgr::GetDisplayAttributeTrackPropertyRange -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CicDisplayAttributeMgr::GetDisplayAttributeTrackPropertyRange(TfEditCookie ec, - ITfContext* pic, - ITfRange* pRange, - ITfReadOnlyProperty** ppProp, - IEnumTfRanges** ppEnum, - ULONG* pulNumProp) -{ - auto hr = E_FAIL; - try - { - auto ulNumProp = static_cast(m_DispAttrProp.size()); - if (ulNumProp) - { - // TrackProperties wants an array of GUID *'s - auto ppguidProp = std::make_unique(ulNumProp); - for (ULONG i = 0; i < ulNumProp; i++) - { - ppguidProp[i] = &m_DispAttrProp.at(i); - } - - wil::com_ptr pProp; - if (SUCCEEDED(hr = pic->TrackProperties(ppguidProp.get(), ulNumProp, nullptr, NULL, &pProp))) - { - hr = pProp->EnumRanges(ec, ppEnum, pRange); - if (SUCCEEDED(hr)) - { - *ppProp = pProp.detach(); - } - } - - if (SUCCEEDED(hr)) - { - *pulNumProp = ulNumProp; - } - } - } - CATCH_RETURN(); - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CicDisplayAttributeMgr::GetDisplayAttributeData -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CicDisplayAttributeMgr::GetDisplayAttributeData(ITfCategoryMgr* pcat, - TfEditCookie ec, - ITfReadOnlyProperty* pProp, - ITfRange* pRange, - TF_DISPLAYATTRIBUTE* pda, - TfGuidAtom* pguid, - ULONG /*ulNumProp*/) -{ - VARIANT var; - - auto hr = E_FAIL; - - if (SUCCEEDED(pProp->GetValue(ec, pRange, &var))) - { - FAIL_FAST_IF(!(var.vt == VT_UNKNOWN)); - - wil::com_ptr_nothrow pEnumPropertyVal; - if (wil::try_com_query_to(var.punkVal, &pEnumPropertyVal)) - { - TF_PROPERTYVAL tfPropVal; - while (pEnumPropertyVal->Next(1, &tfPropVal, nullptr) == S_OK) - { - if (tfPropVal.varValue.vt == VT_EMPTY) - { - continue; // prop has no value over this span - } - - FAIL_FAST_IF(!(tfPropVal.varValue.vt == VT_I4)); // expecting GUIDATOMs - - auto gaVal = (TfGuidAtom)tfPropVal.varValue.lVal; - - GUID guid; - pcat->GetGUID(gaVal, &guid); - - wil::com_ptr_nothrow pDAI; - if (SUCCEEDED(m_pDAM->GetDisplayAttributeInfo(guid, &pDAI, NULL))) - { - // - // Issue: for simple apps. - // - // Small apps can not show multi underline. So - // this helper function returns only one - // DISPLAYATTRIBUTE structure. - // - if (pda) - { - pDAI->GetAttributeInfo(pda); - } - - if (pguid) - { - *pguid = gaVal; - } - - hr = S_OK; - break; - } - } - } - VariantClear(&var); - } - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CicDisplayAttributeMgr::InitCategoryInstance -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CicDisplayAttributeMgr::InitDisplayAttributeInstance(ITfCategoryMgr* pcat) -{ - HRESULT hr; - - // - // Create ITfDisplayAttributeMgr instance. - // - if (FAILED(hr = ::CoCreateInstance(CLSID_TF_DisplayAttributeMgr, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&m_pDAM)))) - { - return hr; - } - - wil::com_ptr_nothrow pEnumProp; - pcat->EnumItemsInCategory(GUID_TFCAT_DISPLAYATTRIBUTEPROPERTY, &pEnumProp); - - // - // make a database for Display Attribute Properties. - // - if (pEnumProp) - { - GUID guidProp; - - try - { - // - // add System Display Attribute first. - // so no other Display Attribute property overwrite it. - // - m_DispAttrProp.emplace_back(GUID_PROP_ATTRIBUTE); - - while (pEnumProp->Next(1, &guidProp, nullptr) == S_OK) - { - if (!IsEqualGUID(guidProp, GUID_PROP_ATTRIBUTE)) - { - m_DispAttrProp.emplace_back(guidProp); - } - } - } - CATCH_RETURN(); - } - return hr; -} diff --git a/src/tsf/TfDispAttr.h b/src/tsf/TfDispAttr.h deleted file mode 100644 index 74fc4dd7f2..0000000000 --- a/src/tsf/TfDispAttr.h +++ /dev/null @@ -1,51 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfDispAttr.h - -Abstract: - - This file defines the CicDisplayAttributeMgr Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#pragma once - -class CicDisplayAttributeMgr -{ -public: - CicDisplayAttributeMgr(); - virtual ~CicDisplayAttributeMgr(); - -public: - [[nodiscard]] HRESULT GetDisplayAttributeTrackPropertyRange(TfEditCookie ec, - ITfContext* pic, - ITfRange* pRange, - ITfReadOnlyProperty** ppProp, - IEnumTfRanges** ppEnum, - ULONG* pulNumProp); - [[nodiscard]] HRESULT GetDisplayAttributeData(ITfCategoryMgr* pcat, - TfEditCookie ec, - ITfReadOnlyProperty* pProp, - ITfRange* pRange, - TF_DISPLAYATTRIBUTE* pda, - TfGuidAtom* pguid, - ULONG ulNumProp); - [[nodiscard]] HRESULT InitDisplayAttributeInstance(ITfCategoryMgr* pcat); - - inline ITfDisplayAttributeMgr* GetDisplayAttributeMgr() { return m_pDAM.get(); } - -private: - wil::com_ptr_nothrow m_pDAM; - std::vector m_DispAttrProp; -}; diff --git a/src/tsf/TfEditSession.cpp b/src/tsf/TfEditSession.cpp deleted file mode 100644 index 2e728d0c9e..0000000000 --- a/src/tsf/TfEditSession.cpp +++ /dev/null @@ -1,1167 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfEditSession.cpp - -Abstract: - - This file implements the CEditSessionObject Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#include "precomp.h" -#include "TfConvArea.h" -#include "TfCatUtil.h" -#include "TfDispAttr.h" -#include "TfEditSession.h" - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::IUnknown::QueryInterface -// CEditSessionObject::IUnknown::AddRef -// CEditSessionObject::IUnknown::Release -// -//---------------------------------------------------------------------------- - -STDAPI CEditSessionObject::QueryInterface(REFIID riid, void** ppvObj) -{ - *ppvObj = nullptr; - - if (IsEqualIID(riid, IID_ITfEditSession)) - { - *ppvObj = static_cast(this); - } - else if (IsEqualIID(riid, IID_IUnknown)) - { - *ppvObj = static_cast(this); - } - - if (*ppvObj) - { - AddRef(); - return S_OK; - } - - return E_NOINTERFACE; -} - -STDAPI_(ULONG) -CEditSessionObject::AddRef() -{ - return ++m_cRef; -} - -STDAPI_(ULONG) -CEditSessionObject::Release() -{ - long cr; - - cr = --m_cRef; - FAIL_FAST_IF(!(cr >= 0)); - - if (cr == 0) - { - delete this; - } - - return cr; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::GetAllTextRange -// -//---------------------------------------------------------------------------- - -// static -[[nodiscard]] HRESULT CEditSessionObject::GetAllTextRange(TfEditCookie ec, - ITfContext* ic, - ITfRange** range, - LONG* lpTextLength, - TF_HALTCOND* lpHaltCond) -{ - HRESULT hr; - - // - // init lpTextLength first. - // - *lpTextLength = 0; - - // - // Create the range that covers all the text. - // - wil::com_ptr_nothrow rangeFull; - if (FAILED(hr = ic->GetStart(ec, &rangeFull))) - { - return hr; - } - - LONG cch = 0; - if (FAILED(hr = rangeFull->ShiftEnd(ec, LONG_MAX, &cch, lpHaltCond))) - { - return hr; - } - - if (FAILED(hr = rangeFull->Clone(range))) - { - return hr; - } - - *lpTextLength = cch; - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::SetTextInRange -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionObject::SetTextInRange(TfEditCookie ec, - ITfRange* range, - __in_ecount_opt(len) LPWSTR psz, - DWORD len) -{ - auto hr = E_FAIL; - if (g_pConsoleTSF) - { - g_pConsoleTSF->SetModifyingDocFlag(TRUE); - hr = range->SetText(ec, 0, psz, len); - g_pConsoleTSF->SetModifyingDocFlag(FALSE); - } - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::ClearTextInRange -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionObject::ClearTextInRange(TfEditCookie ec, ITfRange* range) -{ - // - // Clear the text in Cicero TOM - // - return SetTextInRange(ec, range, nullptr, 0); -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::_GetCursorPosition -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionObject::_GetCursorPosition(TfEditCookie ec, CCompCursorPos& CompCursorPos) -{ - auto pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : nullptr; - if (pic == nullptr) - { - return E_FAIL; - } - - HRESULT hr; - ULONG cFetched; - - TF_SELECTION sel; - sel.range = nullptr; - - if (SUCCEEDED(hr = pic->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &sel, &cFetched))) - { - wil::com_ptr_nothrow start; - LONG ich; - TF_HALTCOND hc; - - hc.pHaltRange = sel.range; - hc.aHaltPos = (sel.style.ase == TF_AE_START) ? TF_ANCHOR_START : TF_ANCHOR_END; - hc.dwFlags = 0; - - if (SUCCEEDED(hr = GetAllTextRange(ec, pic, &start, &ich, &hc))) - { - CompCursorPos.SetCursorPosition(ich); - } - - SafeReleaseClear(sel.range); - } - - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::_GetTextAndAttribute -// -//---------------------------------------------------------------------------- - -// -// Get text and attribute in given range -// -// ITfRange::range -// TF_ANCHOR_START -// |======================================================================| -// +--------------------+ #+----------+ -// |ITfRange::pPropRange| #|pPropRange| -// +--------------------+ #+----------+ -// | GUID_ATOM | # -// +--------------------+ # -// ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^# -// ITfRange::gap_range gap_range # -// # -// V -// ITfRange::no_display_attribute_range -// result_comp -// +1 <- 0 -> -1 -// - -[[nodiscard]] HRESULT CEditSessionObject::_GetTextAndAttribute(TfEditCookie ec, - ITfRange* rangeIn, - std::wstring& CompStr, - std::vector& CompGuid, - std::wstring& ResultStr, - BOOL bInWriteSession, - CicCategoryMgr* pCicCatMgr, - CicDisplayAttributeMgr* pCicDispAttr) -{ - HRESULT hr; - - auto pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : nullptr; - if (pic == nullptr) - { - return E_FAIL; - } - - // - // Get no display attribute range if there exist. - // Otherwise, result range is the same to input range. - // - LONG result_comp; - wil::com_ptr_nothrow no_display_attribute_range; - if (FAILED(hr = rangeIn->Clone(&no_display_attribute_range))) - { - return hr; - } - - const GUID* guids[] = { &GUID_PROP_COMPOSING }; - const int guid_size = sizeof(guids) / sizeof(GUID*); - - if (FAILED(hr = _GetNoDisplayAttributeRange(ec, rangeIn, guids, guid_size, no_display_attribute_range.get()))) - { - return hr; - } - - wil::com_ptr_nothrow propComp; - if (FAILED(hr = pic->TrackProperties(guids, guid_size, // system property - NULL, - 0, // application property - &propComp))) - { - return hr; - } - - wil::com_ptr_nothrow enumComp; - if (FAILED(hr = propComp->EnumRanges(ec, &enumComp, rangeIn))) - { - return hr; - } - - wil::com_ptr_nothrow range; - while (enumComp->Next(1, &range, nullptr) == S_OK) - { - VARIANT var; - auto fCompExist = FALSE; - - hr = propComp->GetValue(ec, range.get(), &var); - if (S_OK == hr) - { - wil::com_ptr_nothrow EnumPropVal; - if (wil::try_com_query_to(var.punkVal, &EnumPropVal)) - { - TF_PROPERTYVAL tfPropertyVal; - - while (EnumPropVal->Next(1, &tfPropertyVal, nullptr) == S_OK) - { - for (auto i = 0; i < guid_size; i++) - { - if (IsEqualGUID(tfPropertyVal.guidId, *guids[i])) - { - if ((V_VT(&tfPropertyVal.varValue) == VT_I4 && V_I4(&tfPropertyVal.varValue) != 0)) - { - fCompExist = TRUE; - break; - } - } - } - - VariantClear(&tfPropertyVal.varValue); - - if (fCompExist) - { - break; - } - } - } - } - - VariantClear(&var); - - ULONG ulNumProp; - - wil::com_ptr_nothrow enumProp; - wil::com_ptr_nothrow prop; - if (FAILED(hr = pCicDispAttr->GetDisplayAttributeTrackPropertyRange(ec, pic, range.get(), &prop, &enumProp, &ulNumProp))) - { - return hr; - } - - // use text range for get text - wil::com_ptr_nothrow textRange; - if (FAILED(hr = range->Clone(&textRange))) - { - return hr; - } - - // use text range for gap text (no property range). - wil::com_ptr_nothrow gap_range; - if (FAILED(hr = range->Clone(&gap_range))) - { - return hr; - } - - wil::com_ptr_nothrow pPropRange; - while (enumProp->Next(1, &pPropRange, nullptr) == S_OK) - { - // pick up the gap up to the next property - gap_range->ShiftEndToRange(ec, pPropRange.get(), TF_ANCHOR_START); - - // - // GAP range - // - no_display_attribute_range->CompareStart(ec, gap_range.get(), TF_ANCHOR_START, &result_comp); - LOG_IF_FAILED(_GetTextAndAttributeGapRange(ec, - gap_range.get(), - result_comp, - CompStr, - CompGuid, - ResultStr)); - - // - // Get display attribute data if some GUID_ATOM exist. - // - TF_DISPLAYATTRIBUTE da; - auto guidatom = TF_INVALID_GUIDATOM; - - LOG_IF_FAILED(pCicDispAttr->GetDisplayAttributeData(pCicCatMgr->GetCategoryMgr(), - ec, - prop.get(), - pPropRange.get(), - &da, - &guidatom, - ulNumProp)); - - // - // Property range - // - no_display_attribute_range->CompareStart(ec, pPropRange.get(), TF_ANCHOR_START, &result_comp); - - // Adjust GAP range's start anchor to the end of property range. - gap_range->ShiftStartToRange(ec, pPropRange.get(), TF_ANCHOR_END); - - // - // Get property text - // - LOG_IF_FAILED(_GetTextAndAttributePropertyRange(ec, - pPropRange.get(), - fCompExist, - result_comp, - bInWriteSession, - da, - guidatom, - CompStr, - CompGuid, - ResultStr)); - - } // while - - // the last non-attr - textRange->ShiftStartToRange(ec, gap_range.get(), TF_ANCHOR_START); - textRange->ShiftEndToRange(ec, range.get(), TF_ANCHOR_END); - - BOOL fEmpty; - while (textRange->IsEmpty(ec, &fEmpty) == S_OK && !fEmpty) - { - WCHAR wstr0[256 + 1]; - ULONG ulcch0 = ARRAYSIZE(wstr0) - 1; - textRange->GetText(ec, TF_TF_MOVESTART, wstr0, ulcch0, &ulcch0); - - TfGuidAtom guidatom; - guidatom = TF_INVALID_GUIDATOM; - - TF_DISPLAYATTRIBUTE da; - da.bAttr = TF_ATTR_INPUT; - - try - { - CompGuid.insert(CompGuid.end(), ulcch0, guidatom); - CompStr.append(wstr0, ulcch0); - } - CATCH_RETURN(); - } - - textRange->Collapse(ec, TF_ANCHOR_END); - - } // out-most while for GUID_PROP_COMPOSING - - // - // set GUID_PROP_CONIME_TRACKCOMPOSITION - // - wil::com_ptr_nothrow PropertyTrackComposition; - if (SUCCEEDED(hr = pic->GetProperty(GUID_PROP_CONIME_TRACKCOMPOSITION, &PropertyTrackComposition))) - { - VARIANT var; - var.vt = VT_I4; - var.lVal = 1; - PropertyTrackComposition->SetValue(ec, rangeIn, &var); - } - - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::_GetTextAndAttributeGapRange -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionObject::_GetTextAndAttributeGapRange(TfEditCookie ec, - ITfRange* gap_range, - LONG result_comp, - std::wstring& CompStr, - std::vector& CompGuid, - std::wstring& ResultStr) -{ - TfGuidAtom guidatom; - guidatom = TF_INVALID_GUIDATOM; - - TF_DISPLAYATTRIBUTE da; - da.bAttr = TF_ATTR_INPUT; - - BOOL fEmpty; - WCHAR wstr0[256 + 1]; - ULONG ulcch0; - - while (gap_range->IsEmpty(ec, &fEmpty) == S_OK && !fEmpty) - { - wil::com_ptr_nothrow backup_range; - if (FAILED(gap_range->Clone(&backup_range))) - { - return E_FAIL; - } - - // - // Retrieve gap text if there exist. - // - ulcch0 = ARRAYSIZE(wstr0) - 1; - if (FAILED(gap_range->GetText(ec, - TF_TF_MOVESTART, // Move range to next after get text. - wstr0, - ulcch0, - &ulcch0))) - { - return E_FAIL; - } - - try - { - if (result_comp <= 0) - { - CompGuid.insert(CompGuid.end(), ulcch0, guidatom); - CompStr.append(wstr0, ulcch0); - } - else - { - ResultStr.append(wstr0, ulcch0); - LOG_IF_FAILED(ClearTextInRange(ec, backup_range.get())); - } - } - CATCH_RETURN(); - } - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::_GetTextAndAttributePropertyRange -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionObject::_GetTextAndAttributePropertyRange(TfEditCookie ec, - ITfRange* pPropRange, - BOOL fCompExist, - LONG result_comp, - BOOL bInWriteSession, - TF_DISPLAYATTRIBUTE da, - TfGuidAtom guidatom, - std::wstring& CompStr, - std::vector& CompGuid, - std::wstring& ResultStr) -{ - BOOL fEmpty; - WCHAR wstr0[256 + 1]; - ULONG ulcch0; - - while (pPropRange->IsEmpty(ec, &fEmpty) == S_OK && !fEmpty) - { - wil::com_ptr_nothrow backup_range; - if (FAILED(pPropRange->Clone(&backup_range))) - { - return E_FAIL; - } - - // - // Retrieve property text if there exist. - // - ulcch0 = ARRAYSIZE(wstr0) - 1; - if (FAILED(pPropRange->GetText(ec, - TF_TF_MOVESTART, // Move range to next after get text. - wstr0, - ulcch0, - &ulcch0))) - { - return E_FAIL; - } - - try - { - // see if there is a valid disp attribute - if (fCompExist == TRUE && result_comp <= 0) - { - if (guidatom == TF_INVALID_GUIDATOM) - { - da.bAttr = TF_ATTR_INPUT; - } - CompGuid.insert(CompGuid.end(), ulcch0, guidatom); - CompStr.append(wstr0, ulcch0); - } - else if (bInWriteSession) - { - // if there's no disp attribute attached, it probably means - // the part of string is finalized. - // - ResultStr.append(wstr0, ulcch0); - - // it was a 'determined' string - // so the doc has to shrink - // - LOG_IF_FAILED(ClearTextInRange(ec, backup_range.get())); - } - else - { - // - // Prevent infinite loop - // - break; - } - } - CATCH_RETURN(); - } - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject::_GetNoDisplayAttributeRange -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionObject::_GetNoDisplayAttributeRange(TfEditCookie ec, - ITfRange* rangeIn, - const GUID** guids, - const int guid_size, - ITfRange* no_display_attribute_range) -{ - auto pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : nullptr; - if (pic == nullptr) - { - return E_FAIL; - } - - wil::com_ptr_nothrow propComp; - auto hr = pic->TrackProperties(guids, guid_size, // system property - nullptr, - 0, // application property - &propComp); - if (FAILED(hr)) - { - return hr; - } - - wil::com_ptr_nothrow enumComp; - hr = propComp->EnumRanges(ec, &enumComp, rangeIn); - if (FAILED(hr)) - { - return hr; - } - - wil::com_ptr_nothrow pRange; - - while (enumComp->Next(1, &pRange, nullptr) == S_OK) - { - VARIANT var; - auto fCompExist = FALSE; - - hr = propComp->GetValue(ec, pRange.get(), &var); - if (S_OK == hr) - { - wil::com_ptr_nothrow EnumPropVal; - if (wil::try_com_query_to(var.punkVal, &EnumPropVal)) - { - TF_PROPERTYVAL tfPropertyVal; - - while (EnumPropVal->Next(1, &tfPropertyVal, nullptr) == S_OK) - { - for (auto i = 0; i < guid_size; i++) - { - if (IsEqualGUID(tfPropertyVal.guidId, *guids[i])) - { - if ((V_VT(&tfPropertyVal.varValue) == VT_I4 && V_I4(&tfPropertyVal.varValue) != 0)) - { - fCompExist = TRUE; - break; - } - } - } - - VariantClear(&tfPropertyVal.varValue); - - if (fCompExist) - { - break; - } - } - } - } - - if (!fCompExist) - { - // Adjust GAP range's start anchor to the end of property range. - no_display_attribute_range->ShiftStartToRange(ec, pRange.get(), TF_ANCHOR_START); - } - - VariantClear(&var); - } - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionCompositionComplete::CompComplete -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionCompositionComplete::CompComplete(TfEditCookie ec) -{ - auto pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : nullptr; - RETURN_HR_IF_NULL(E_FAIL, pic); - - // Get the whole text, finalize it, and set empty string in TOM - wil::com_ptr_nothrow spRange; - LONG cch; - - RETURN_IF_FAILED(GetAllTextRange(ec, pic, &spRange, &cch)); - - // Check if a part of the range has already been finalized but not removed yet. - // Adjust the range appropriately to avoid inserting the same text twice. - auto cchCompleted = g_pConsoleTSF->GetCompletedRangeLength(); - if ((cchCompleted > 0) && - (cchCompleted < cch) && - SUCCEEDED(spRange->ShiftStart(ec, cchCompleted, &cchCompleted, NULL))) - { - FAIL_FAST_IF(!((cchCompleted > 0) && (cchCompleted < cch))); - cch -= cchCompleted; - } - else - { - cchCompleted = 0; - } - - // Get conversion area service. - auto conv_area = g_pConsoleTSF->GetConversionArea(); - RETURN_HR_IF_NULL(E_FAIL, conv_area); - - // If there is no string in TextStore we don't have to do anything. - if (!cch) - { - // Clear composition - LOG_IF_FAILED(conv_area->ClearComposition()); - return S_OK; - } - - auto hr = S_OK; - try - { - auto wstr = std::make_unique(cch + 1); - - // Get the whole text, finalize it, and erase the whole text. - if (SUCCEEDED(spRange->GetText(ec, TF_TF_IGNOREEND, wstr.get(), (ULONG)cch, (ULONG*)&cch))) - { - // Make Result String. - hr = conv_area->DrawResult({ wstr.get(), static_cast(cch) }); - } - } - CATCH_RETURN(); - - // Update the stored length of the completed fragment. - g_pConsoleTSF->SetCompletedRangeLength(cchCompleted + cch); - - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionCompositionCleanup::EmptyCompositionRange() -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionCompositionCleanup::EmptyCompositionRange(TfEditCookie ec) -{ - if (!g_pConsoleTSF) - { - return E_FAIL; - } - if (!g_pConsoleTSF->IsPendingCompositionCleanup()) - { - return S_OK; - } - - auto hr = E_FAIL; - auto pic = g_pConsoleTSF->GetInputContext(); - if (pic != nullptr) - { - // Cleanup (empty the context range) after the last composition. - - hr = S_OK; - auto cchCompleted = g_pConsoleTSF->GetCompletedRangeLength(); - if (cchCompleted != 0) - { - wil::com_ptr_nothrow spRange; - LONG cch; - hr = GetAllTextRange(ec, pic, &spRange, &cch); - if (SUCCEEDED(hr)) - { - // Clean up only the completed part (which start is expected to coincide with the start of the full range). - if (cchCompleted < cch) - { - spRange->ShiftEnd(ec, (cchCompleted - cch), &cch, nullptr); - } - hr = ClearTextInRange(ec, spRange.get()); - g_pConsoleTSF->SetCompletedRangeLength(0); // cleaned up all completed text - } - } - } - g_pConsoleTSF->OnCompositionCleanup(SUCCEEDED(hr)); - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionUpdateCompositionString::UpdateCompositionString -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionUpdateCompositionString::UpdateCompositionString(TfEditCookie ec) -{ - HRESULT hr; - - auto pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : nullptr; - if (pic == nullptr) - { - return E_FAIL; - } - - // Reset the 'edit session requested' flag. - g_pConsoleTSF->OnEditSession(); - - // If the composition has been cancelled\finalized, no update necessary. - if (!g_pConsoleTSF->IsInComposition()) - { - return S_OK; - } - - BOOL bInWriteSession; - if (FAILED(hr = pic->InWriteSession(g_pConsoleTSF->GetTfClientId(), &bInWriteSession))) - { - return hr; - } - - wil::com_ptr_nothrow FullTextRange; - LONG lTextLength; - if (FAILED(hr = GetAllTextRange(ec, pic, &FullTextRange, &lTextLength))) - { - return hr; - } - - wil::com_ptr_nothrow InterimRange; - auto fInterim = FALSE; - if (FAILED(hr = _IsInterimSelection(ec, &InterimRange, &fInterim))) - { - return hr; - } - - CicCategoryMgr* pCicCat = nullptr; - CicDisplayAttributeMgr* pDispAttr = nullptr; - - // - // Create Cicero Category Manager and Display Attribute Manager - // - hr = _CreateCategoryAndDisplayAttributeManager(&pCicCat, &pDispAttr); - if (SUCCEEDED(hr)) - { - if (fInterim) - { - hr = _MakeInterimString(ec, - FullTextRange.get(), - InterimRange.get(), - lTextLength, - bInWriteSession, - pCicCat, - pDispAttr); - } - else - { - hr = _MakeCompositionString(ec, FullTextRange.get(), bInWriteSession, pCicCat, pDispAttr); - } - } - - if (pCicCat) - { - delete pCicCat; - } - if (pDispAttr) - { - delete pDispAttr; - } - - return hr; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionUpdateCompositionString::_IsInterimSelection -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionUpdateCompositionString::_IsInterimSelection(TfEditCookie ec, - ITfRange** pInterimRange, - BOOL* pfInterim) -{ - auto pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : nullptr; - if (pic == nullptr) - { - return E_FAIL; - } - - ULONG cFetched; - - TF_SELECTION sel; - sel.range = nullptr; - - *pfInterim = FALSE; - if (pic->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &sel, &cFetched) != S_OK) - { - // no selection. we can return S_OK. - return S_OK; - } - - if (sel.style.fInterimChar && sel.range) - { - HRESULT hr; - if (FAILED(hr = sel.range->Clone(pInterimRange))) - { - SafeReleaseClear(sel.range); - return hr; - } - - *pfInterim = TRUE; - } - - SafeReleaseClear(sel.range); - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionUpdateCompositionString::_MakeCompositionString -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionUpdateCompositionString::_MakeCompositionString(TfEditCookie ec, - ITfRange* FullTextRange, - BOOL bInWriteSession, - CicCategoryMgr* pCicCatMgr, - CicDisplayAttributeMgr* pCicDispAttr) -{ - std::wstring CompStr; - std::vector CompGuid; - CCompCursorPos CompCursorPos; - std::wstring ResultStr; - auto fIgnorePreviousCompositionResult = FALSE; - - RETURN_IF_FAILED(_GetTextAndAttribute(ec, - FullTextRange, - CompStr, - CompGuid, - ResultStr, - bInWriteSession, - pCicCatMgr, - pCicDispAttr)); - - if (g_pConsoleTSF && g_pConsoleTSF->IsPendingCompositionCleanup()) - { - // Don't draw the previous composition result if there was a cleanup session requested for it. - fIgnorePreviousCompositionResult = TRUE; - // Cancel pending cleanup, since the ResultStr was cleared from the composition in _GetTextAndAttribute. - g_pConsoleTSF->OnCompositionCleanup(TRUE); - } - - RETURN_IF_FAILED(_GetCursorPosition(ec, CompCursorPos)); - - // Get display attribute manager - auto dam = pCicDispAttr->GetDisplayAttributeMgr(); - RETURN_HR_IF_NULL(E_FAIL, dam); - - // Get category manager - auto cat = pCicCatMgr->GetCategoryMgr(); - RETURN_HR_IF_NULL(E_FAIL, cat); - - // Allocate and fill TF_DISPLAYATTRIBUTE - try - { - // Get conversion area service. - auto conv_area = g_pConsoleTSF ? g_pConsoleTSF->GetConversionArea() : nullptr; - RETURN_HR_IF_NULL(E_FAIL, conv_area); - - if (!ResultStr.empty() && !fIgnorePreviousCompositionResult) - { - return conv_area->DrawResult(ResultStr); - } - if (!CompStr.empty()) - { - const auto cchDisplayAttribute = CompGuid.size(); - std::vector DisplayAttributes; - DisplayAttributes.reserve(cchDisplayAttribute); - - for (size_t i = 0; i < cchDisplayAttribute; i++) - { - TF_DISPLAYATTRIBUTE da; - ZeroMemory(&da, sizeof(da)); - da.bAttr = TF_ATTR_OTHER; - - GUID guid; - if (SUCCEEDED(cat->GetGUID(CompGuid.at(i), &guid))) - { - CLSID clsid; - wil::com_ptr_nothrow dai; - if (SUCCEEDED(dam->GetDisplayAttributeInfo(guid, &dai, &clsid))) - { - dai->GetAttributeInfo(&da); - } - } - - DisplayAttributes.emplace_back(da); - } - - return conv_area->DrawComposition(CompStr, // composition string - DisplayAttributes, // display attributes - CompCursorPos.GetCursorPosition()); // cursor position - } - } - CATCH_RETURN(); - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionUpdateCompositionString::_MakeInterimString -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionUpdateCompositionString::_MakeInterimString(TfEditCookie ec, - ITfRange* FullTextRange, - ITfRange* InterimRange, - LONG lTextLength, - BOOL bInWriteSession, - CicCategoryMgr* pCicCatMgr, - CicDisplayAttributeMgr* pCicDispAttr) -{ - LONG lStartResult; - LONG lEndResult; - - FullTextRange->CompareStart(ec, InterimRange, TF_ANCHOR_START, &lStartResult); - RETURN_HR_IF(E_FAIL, lStartResult > 0); - - FullTextRange->CompareEnd(ec, InterimRange, TF_ANCHOR_END, &lEndResult); - RETURN_HR_IF(E_FAIL, lEndResult < 0); - - if (lStartResult < 0) - { - // Make result string. - RETURN_IF_FAILED(FullTextRange->ShiftEndToRange(ec, InterimRange, TF_ANCHOR_START)); - - // Interim char assume 1 char length. - // Full text length - 1 means result string length. - lTextLength--; - - FAIL_FAST_IF(!(lTextLength > 0)); - - if (lTextLength > 0) - { - try - { - auto wstr = std::make_unique(lTextLength + 1); - - // Get the result text, finalize it, and erase the result text. - if (SUCCEEDED(FullTextRange->GetText(ec, TF_TF_IGNOREEND, wstr.get(), (ULONG)lTextLength, (ULONG*)&lTextLength))) - { - // Clear the TOM - LOG_IF_FAILED(ClearTextInRange(ec, FullTextRange)); - } - } - CATCH_RETURN(); - } - } - - // Make interim character - std::wstring CompStr; - std::vector CompGuid; - std::wstring _tempResultStr; - - RETURN_IF_FAILED(_GetTextAndAttribute(ec, - InterimRange, - CompStr, - CompGuid, - _tempResultStr, - bInWriteSession, - pCicCatMgr, - pCicDispAttr)); - - // Get display attribute manager - auto dam = pCicDispAttr->GetDisplayAttributeMgr(); - RETURN_HR_IF_NULL(E_FAIL, dam); - - // Get category manager - auto cat = pCicCatMgr->GetCategoryMgr(); - RETURN_HR_IF_NULL(E_FAIL, cat); - - // Allocate and fill TF_DISPLAYATTRIBUTE - try - { - // Get conversion area service. - auto conv_area = g_pConsoleTSF ? g_pConsoleTSF->GetConversionArea() : nullptr; - RETURN_HR_IF_NULL(E_FAIL, conv_area); - - if (!CompStr.empty()) - { - const auto cchDisplayAttribute = CompGuid.size(); - std::vector DisplayAttributes; - DisplayAttributes.reserve(cchDisplayAttribute); - - for (size_t i = 0; i < cchDisplayAttribute; i++) - { - TF_DISPLAYATTRIBUTE da; - ZeroMemory(&da, sizeof(da)); - da.bAttr = TF_ATTR_OTHER; - GUID guid; - if (SUCCEEDED(cat->GetGUID(CompGuid.at(i), &guid))) - { - CLSID clsid; - wil::com_ptr_nothrow dai; - if (SUCCEEDED(dam->GetDisplayAttributeInfo(guid, &dai, &clsid))) - { - dai->GetAttributeInfo(&da); - } - } - - DisplayAttributes.emplace_back(da); - } - - return conv_area->DrawComposition(CompStr, // composition string (Interim string) - DisplayAttributes); // display attributes - } - } - CATCH_RETURN(); - - return S_OK; -} - -//+--------------------------------------------------------------------------- -// -// CEditSessionUpdateCompositionString::_CreateCategoryAndDisplayAttributeManager -// -//---------------------------------------------------------------------------- - -[[nodiscard]] HRESULT CEditSessionUpdateCompositionString::_CreateCategoryAndDisplayAttributeManager( - CicCategoryMgr** pCicCatMgr, - CicDisplayAttributeMgr** pCicDispAttr) -{ - auto hr = E_OUTOFMEMORY; - - CicCategoryMgr* pTmpCat = nullptr; - CicDisplayAttributeMgr* pTmpDispAttr = nullptr; - - // - // Create Cicero Category Manager - // - pTmpCat = new (std::nothrow) CicCategoryMgr; - if (pTmpCat) - { - if (SUCCEEDED(hr = pTmpCat->InitCategoryInstance())) - { - auto pcat = pTmpCat->GetCategoryMgr(); - if (pcat) - { - // - // Create Cicero Display Attribute Manager - // - pTmpDispAttr = new (std::nothrow) CicDisplayAttributeMgr; - if (pTmpDispAttr) - { - if (SUCCEEDED(hr = pTmpDispAttr->InitDisplayAttributeInstance(pcat))) - { - *pCicCatMgr = pTmpCat; - *pCicDispAttr = pTmpDispAttr; - } - } - } - } - } - - if (FAILED(hr)) - { - if (pTmpCat) - { - delete pTmpCat; - } - if (pTmpDispAttr) - { - delete pTmpDispAttr; - } - } - - return hr; -} diff --git a/src/tsf/TfEditSession.h b/src/tsf/TfEditSession.h deleted file mode 100644 index 61ac5c1cac..0000000000 --- a/src/tsf/TfEditSession.h +++ /dev/null @@ -1,219 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfEditSession.h - -Abstract: - - This file defines the CEditSessionObject Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#pragma once - -class CicCategoryMgr; -class CicDisplayAttributeMgr; - -/* 183C627A-B46C-44ad-B797-82F6BEC82131 */ -const GUID GUID_PROP_CONIME_TRACKCOMPOSITION = { - 0x183c627a, - 0xb46c, - 0x44ad, - { 0xb7, 0x97, 0x82, 0xf6, 0xbe, 0xc8, 0x21, 0x31 } -}; - -//+--------------------------------------------------------------------------- -// -// CEditSessionObject -// -//---------------------------------------------------------------------------- - -class CEditSessionObject : public ITfEditSession -{ -public: - CEditSessionObject() : - m_cRef(1) {} - virtual ~CEditSessionObject() = default; - -public: - // - // IUnknown methods - // - STDMETHODIMP QueryInterface(REFIID riid, void** ppvObj); - STDMETHODIMP_(ULONG) - AddRef(void); - STDMETHODIMP_(ULONG) - Release(void); - - // - // ITfEditSession method - // - STDMETHODIMP DoEditSession(TfEditCookie ec) - { - auto hr = _DoEditSession(ec); - Release(); // Release reference count for asynchronous edit session. - return hr; - } - - // - // ImmIfSessionObject methods - // -protected: - [[nodiscard]] virtual HRESULT _DoEditSession(TfEditCookie ec) = 0; - - // - // EditSession methods. - // -public: - [[nodiscard]] static HRESULT GetAllTextRange(TfEditCookie ec, - ITfContext* ic, - ITfRange** range, - LONG* lpTextLength, - TF_HALTCOND* lpHaltCond = nullptr); - -protected: - [[nodiscard]] HRESULT SetTextInRange(TfEditCookie ec, - ITfRange* range, - __in_ecount_opt(len) LPWSTR psz, - DWORD len); - [[nodiscard]] HRESULT ClearTextInRange(TfEditCookie ec, - ITfRange* range); - - [[nodiscard]] HRESULT _GetTextAndAttribute(TfEditCookie ec, - ITfRange* range, - std::wstring& CompStr, - std::vector CompGuid, - BOOL bInWriteSession, - CicCategoryMgr* pCicCatMgr, - CicDisplayAttributeMgr* pCicDispAttr) - { - std::wstring ResultStr; - return _GetTextAndAttribute(ec, range, CompStr, CompGuid, ResultStr, bInWriteSession, pCicCatMgr, pCicDispAttr); - } - - [[nodiscard]] HRESULT _GetTextAndAttribute(TfEditCookie ec, - ITfRange* range, - std::wstring& CompStr, - std::vector& CompGuid, - std::wstring& ResultStr, - BOOL bInWriteSession, - CicCategoryMgr* pCicCatMgr, - CicDisplayAttributeMgr* pCicDispAttr); - - [[nodiscard]] HRESULT _GetTextAndAttributeGapRange(TfEditCookie ec, - ITfRange* gap_range, - LONG result_comp, - std::wstring& CompStr, - std::vector& CompGuid, - std::wstring& ResultStr); - - [[nodiscard]] HRESULT _GetTextAndAttributePropertyRange(TfEditCookie ec, - ITfRange* pPropRange, - BOOL fDispAttribute, - LONG result_comp, - BOOL bInWriteSession, - TF_DISPLAYATTRIBUTE da, - TfGuidAtom guidatom, - std::wstring& CompStr, - std::vector& CompGuid, - std::wstring& ResultStr); - - [[nodiscard]] HRESULT _GetNoDisplayAttributeRange(TfEditCookie ec, - ITfRange* range, - const GUID** guids, - const int guid_size, - ITfRange* no_display_attribute_range); - - [[nodiscard]] HRESULT _GetCursorPosition(TfEditCookie ec, - CCompCursorPos& CompCursorPos); - -private: - int m_cRef; -}; - -//+--------------------------------------------------------------------------- -// -// CEditSessionCompositionComplete -// -//---------------------------------------------------------------------------- - -class CEditSessionCompositionComplete : public CEditSessionObject -{ -public: - CEditSessionCompositionComplete() = default; - - [[nodiscard]] HRESULT _DoEditSession(TfEditCookie ec) - { - return CompComplete(ec); - } - - [[nodiscard]] HRESULT CompComplete(TfEditCookie ec); -}; - -//+--------------------------------------------------------------------------- -// -// CEditSessionCompositionCleanup -// -//---------------------------------------------------------------------------- - -class CEditSessionCompositionCleanup : public CEditSessionObject -{ -public: - CEditSessionCompositionCleanup() = default; - - [[nodiscard]] HRESULT _DoEditSession(TfEditCookie ec) - { - return EmptyCompositionRange(ec); - } - - [[nodiscard]] HRESULT EmptyCompositionRange(TfEditCookie ec); -}; - -//+--------------------------------------------------------------------------- -// -// CEditSessionUpdateCompositionString -// -//---------------------------------------------------------------------------- - -class CEditSessionUpdateCompositionString : public CEditSessionObject -{ -public: - CEditSessionUpdateCompositionString() = default; - - [[nodiscard]] HRESULT _DoEditSession(TfEditCookie ec) - { - return UpdateCompositionString(ec); - } - - [[nodiscard]] HRESULT UpdateCompositionString(TfEditCookie ec); - -private: - [[nodiscard]] HRESULT _IsInterimSelection(TfEditCookie ec, ITfRange** pInterimRange, BOOL* pfInterim); - - [[nodiscard]] HRESULT _MakeCompositionString(TfEditCookie ec, - ITfRange* FullTextRange, - BOOL bInWriteSession, - CicCategoryMgr* pCicCatMgr, - CicDisplayAttributeMgr* pCicDispAttr); - - [[nodiscard]] HRESULT _MakeInterimString(TfEditCookie ec, - ITfRange* FullTextRange, - ITfRange* InterimRange, - LONG lTextLength, - BOOL bInWriteSession, - CicCategoryMgr* pCicCatMgr, - CicDisplayAttributeMgr* pCicDispAttr); - - [[nodiscard]] HRESULT _CreateCategoryAndDisplayAttributeManager(CicCategoryMgr** pCicCatMgr, - CicDisplayAttributeMgr** pCicDispAttr); -}; diff --git a/src/tsf/TfTxtevCb.cpp b/src/tsf/TfTxtevCb.cpp deleted file mode 100644 index 90bac9db2d..0000000000 --- a/src/tsf/TfTxtevCb.cpp +++ /dev/null @@ -1,169 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - TfTxtevCb.cpp - -Abstract: - - This file implements the CTextEventSinkCallBack Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#include "precomp.h" -#include "ConsoleTSF.h" -#include "TfEditSession.h" - -//+--------------------------------------------------------------------------- -// -// CConsoleTSF::HasCompositionChanged -// -//---------------------------------------------------------------------------- - -BOOL CConsoleTSF::_HasCompositionChanged(ITfContext* pInputContext, TfEditCookie ecReadOnly, ITfEditRecord* pEditRecord) -{ - BOOL fChanged; - if (SUCCEEDED(pEditRecord->GetSelectionStatus(&fChanged))) - { - if (fChanged) - { - return TRUE; - } - } - - // - // Find GUID_PROP_CONIME_TRACKCOMPOSITION property. - // - - wil::com_ptr_nothrow Property; - wil::com_ptr_nothrow FoundRange; - wil::com_ptr_nothrow PropertyTrackComposition; - - auto bFound = FALSE; - - if (SUCCEEDED(pInputContext->GetProperty(GUID_PROP_CONIME_TRACKCOMPOSITION, &Property))) - { - wil::com_ptr_nothrow EnumFindFirstTrackCompRange; - - if (SUCCEEDED(Property->EnumRanges(ecReadOnly, &EnumFindFirstTrackCompRange, NULL))) - { - HRESULT hr; - wil::com_ptr_nothrow range; - - while ((hr = EnumFindFirstTrackCompRange->Next(1, &range, nullptr)) == S_OK) - { - VARIANT var; - VariantInit(&var); - - hr = Property->GetValue(ecReadOnly, range.get(), &var); - if (SUCCEEDED(hr)) - { - if ((V_VT(&var) == VT_I4 && V_I4(&var) != 0)) - { - range->Clone(&FoundRange); - bFound = TRUE; // FOUND!! - break; - } - } - - VariantClear(&var); - - if (bFound) - { - break; // FOUND!! - } - } - } - } - - // - // if there is no track composition property, - // the composition has been changed since we put it. - // - if (!bFound) - { - return TRUE; - } - - if (FoundRange == nullptr) - { - return FALSE; - } - - bFound = FALSE; // RESET bFound flag... - - wil::com_ptr_nothrow rangeTrackComposition; - if (SUCCEEDED(FoundRange->Clone(&rangeTrackComposition))) - { - // - // get the text range that does not include read only area for - // reconversion. - // - wil::com_ptr_nothrow rangeAllText; - LONG cch; - if (SUCCEEDED(CEditSessionObject::GetAllTextRange(ecReadOnly, pInputContext, &rangeAllText, &cch))) - { - LONG lResult; - if (SUCCEEDED(rangeTrackComposition->CompareStart(ecReadOnly, rangeAllText.get(), TF_ANCHOR_START, &lResult))) - { - // - // if the start position of the track composition range is not - // the beginning of IC, - // the composition has been changed since we put it. - // - if (lResult != 0) - { - bFound = TRUE; // FOUND!! - } - else if (SUCCEEDED(rangeTrackComposition->CompareEnd(ecReadOnly, rangeAllText.get(), TF_ANCHOR_END, &lResult))) - { - // - // if the start position of the track composition range is not - // the beginning of IC, - // the composition has been changed since we put it. - // - // - // If we find the changes in these property, we need to update hIMC. - // - const GUID* guids[] = { &GUID_PROP_COMPOSING, - &GUID_PROP_ATTRIBUTE }; - const int guid_size = sizeof(guids) / sizeof(GUID*); - - wil::com_ptr_nothrow EnumPropertyChanged; - - if (lResult != 0) - { - bFound = TRUE; // FOUND!! - } - else if (SUCCEEDED(pEditRecord->GetTextAndPropertyUpdates(TF_GTP_INCL_TEXT, guids, guid_size, &EnumPropertyChanged))) - { - HRESULT hr; - wil::com_ptr_nothrow range; - - while ((hr = EnumPropertyChanged->Next(1, &range, nullptr)) == S_OK) - { - BOOL empty; - if (range->IsEmpty(ecReadOnly, &empty) == S_OK && empty) - { - continue; - } - - bFound = TRUE; // FOUND!! - break; - } - } - } - } - } - } - return bFound; -} diff --git a/src/tsf/contsf.cpp b/src/tsf/contsf.cpp deleted file mode 100644 index 81a98c5b84..0000000000 --- a/src/tsf/contsf.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -CConsoleTSF* g_pConsoleTSF = nullptr; - -extern "C" BOOL ActivateTextServices(HWND hwndConsole, GetSuggestionWindowPos pfnPosition, GetTextBoxAreaPos pfnTextArea) -{ - if (!g_pConsoleTSF && hwndConsole) - { - g_pConsoleTSF = new (std::nothrow) CConsoleTSF(hwndConsole, pfnPosition, pfnTextArea); - if (g_pConsoleTSF && SUCCEEDED(g_pConsoleTSF->Initialize())) - { - // Conhost calls this function only when the console window has focus. - g_pConsoleTSF->SetFocus(TRUE); - } - else - { - SafeReleaseClear(g_pConsoleTSF); - } - } - return g_pConsoleTSF ? TRUE : FALSE; -} - -extern "C" void DeactivateTextServices() -{ - if (g_pConsoleTSF) - { - g_pConsoleTSF->Uninitialize(); - SafeReleaseClear(g_pConsoleTSF); - } -} diff --git a/src/tsf/globals.h b/src/tsf/globals.h deleted file mode 100644 index 0fc503f070..0000000000 --- a/src/tsf/globals.h +++ /dev/null @@ -1,57 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation. -Licensed under the MIT license. - -Module Name: - - globals.h - -Abstract: - - Contains declarations for all globally scoped names in the program. - This file defines the CBoolean Interface Class. - -Author: - -Revision History: - -Notes: - ---*/ - -#pragma once - -// -// SAFECAST(obj, type) -// -// This macro is extremely useful for enforcing strong typechecking on other -// macros. It generates no code. -// -// Simply insert this macro at the beginning of an expression list for -// each parameter that must be typechecked. For example, for the -// definition of MYMAX(x, y), where x and y absolutely must be integers, -// use: -// -// #define MYMAX(x, y) (SAFECAST(x, int), SAFECAST(y, int), ((x) > (y) ? (x) : (y))) -// -// -#define SAFECAST(_obj, _type) (((_type)(_obj) == (_obj) ? 0 : 0), (_type)(_obj)) - -// -// Bitfields don't get along too well with bools, -// so here's an easy way to convert them: -// -#define BOOLIFY(expr) (!!(expr)) - -// -// generic COM stuff -// -#define SafeReleaseClear(punk) \ - { \ - if ((punk) != NULL) \ - { \ - (punk)->Release(); \ - (punk) = NULL; \ - } \ - } diff --git a/src/tsf/precomp.h b/src/tsf/precomp.h index d81ef6efb8..bf7aac2262 100644 --- a/src/tsf/precomp.h +++ b/src/tsf/precomp.h @@ -37,16 +37,11 @@ extern "C" { #include #include #include +#include +#include #include // Cicero header #include // ITextStore standard attributes // This includes support libraries from the CRT, STL, WIL, and GSL #include "LibraryIncludes.h" - -#include "../inc/contsf.h" - -#include "globals.h" - -#include "ConsoleTSF.h" -#include "TfCtxtComp.h" diff --git a/src/tsf/sources b/src/tsf/sources index 54c7916209..648f380a05 100644 --- a/src/tsf/sources +++ b/src/tsf/sources @@ -42,17 +42,10 @@ PRECOMPILED_PCH = precomp.pch PRECOMPILED_OBJ = precomp.obj SOURCES = \ - contsf.cpp \ - ConsoleTSF.cpp \ - TfConvArea.cpp \ - TfCatUtil.cpp \ - TfDispAttr.cpp \ - TfEditSession.cpp \ - TfTxtevCb.cpp \ + Handle.cpp \ + Implementation.cpp \ INCLUDES = \ $(INCLUDES); \ ..\inc; \ $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ - $(SDK_INC_PATH)\atl30; \ - $(ONECORE_EXTERNAL_SDK_INC_PATH)\atl30; \ diff --git a/src/tsf/tsf.vcxproj b/src/tsf/tsf.vcxproj index 26da838865..97217966bd 100644 --- a/src/tsf/tsf.vcxproj +++ b/src/tsf/tsf.vcxproj @@ -11,29 +11,18 @@ - - - - - - - + + Create - - - + + - - - - - - + \ No newline at end of file diff --git a/src/tsf/tsf.vcxproj.filters b/src/tsf/tsf.vcxproj.filters index 3b4f7739ed..21b300cc00 100644 --- a/src/tsf/tsf.vcxproj.filters +++ b/src/tsf/tsf.vcxproj.filters @@ -15,58 +15,29 @@ - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - Source Files + + Source Files + + + Source Files + - - Header Files - - - Header Files - - - Header Files - Header Files - + Header Files - - Header Files - - - Header Files - - - Header Files - - + Header Files - + + + + + \ No newline at end of file From f49cf44b799b4a4017ab7dce5c29694cda0fe725 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 22 Apr 2024 22:45:10 +0200 Subject: [PATCH 08/53] Add more TraceLogging to ApiDispatchers (#17085) More TraceLogging = More better? I made this change as I noticed that most calls are not being logged. Even after this change some crucial information won't be logged (for instance arrays of `INPUT_RECORD`), because I couldn't come up with a clever way to do so, but I think this is better than nothing. --- src/host/_output.cpp | 1 - src/host/misc.cpp | 2 - src/host/tracing.hpp | 39 ++-- src/propsheet/console.h | 4 - src/server/ApiDispatchers.cpp | 385 +++++++++++++++++++++++++++++++--- 5 files changed, 374 insertions(+), 57 deletions(-) diff --git a/src/host/_output.cpp b/src/host/_output.cpp index 0804a7ffab..44701989a5 100644 --- a/src/host/_output.cpp +++ b/src/host/_output.cpp @@ -30,7 +30,6 @@ using Microsoft::Console::Interactivity::ServiceLocator; // - void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region) { - DBGOUTPUT(("WriteToScreen\n")); const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // update to screen, if we're not iconic. if (!screenInfo.IsActiveScreenBuffer() || WI_IsFlagSet(gci.Flags, CONSOLE_IS_ICONIC)) diff --git a/src/host/misc.cpp b/src/host/misc.cpp index 329328f567..3cadbf9f4c 100644 --- a/src/host/misc.cpp +++ b/src/host/misc.cpp @@ -64,7 +64,6 @@ int ConvertToOem(const UINT uiCodePage, const UINT cchTarget) noexcept { FAIL_FAST_IF(!(pwchSource != (LPWSTR)pchTarget)); - DBGCHARS(("ConvertToOem U->%d %.*ls\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pwchSource)); // clang-format off #pragma prefast(suppress: __WARNING_W2A_BEST_FIT, "WC_NO_BEST_FIT_CHARS doesn't work in many codepages. Retain old behavior.") // clang-format on @@ -80,6 +79,5 @@ int ConvertOutputToUnicode(_In_ UINT uiCodePage, { FAIL_FAST_IF(!(cchTarget > 0)); pwchTarget[0] = L'\0'; - DBGCHARS(("ConvertOutputToUnicode %d->U %.*s\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pchSource)); return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, pchSource, cchSource, pwchTarget, cchTarget); } diff --git a/src/host/tracing.hpp b/src/host/tracing.hpp index 03cf423f4d..7549d54850 100644 --- a/src/host/tracing.hpp +++ b/src/host/tracing.hpp @@ -17,28 +17,29 @@ Author(s): #pragma once -#include - #include "../types/inc/Viewport.hpp" -#if DBG -#define DBGCHARS(_params_) \ - { \ - Tracing::s_TraceChars _params_; \ - } -#define DBGOUTPUT(_params_) \ - { \ - Tracing::s_TraceOutput _params_; \ - } -#else -#define DBGCHARS(_params_) -#define DBGOUTPUT(_params_) -#endif +#define TraceLoggingConsoleCoord(value, name) \ + TraceLoggingPackedData(&value, sizeof(COORD)), \ + TraceLoggingPackedStruct(2, name), \ + TraceLoggingPackedMetadata(TlgInINT16, "X"), \ + TraceLoggingPackedMetadata(TlgInINT16, "Y") -#define TraceLoggingConsoleCoord(value, name) \ - TraceLoggingStruct(2, name), \ - TraceLoggingInt32(value.X, "X"), \ - TraceLoggingInt32(value.Y, "Y") +#define TraceLoggingConsoleSmallRect(value, name) \ + TraceLoggingPackedData(&value, sizeof(SMALL_RECT)), \ + TraceLoggingPackedStruct(4, name), \ + TraceLoggingInt16(TlgInINT16, "Left"), \ + TraceLoggingInt16(TlgInINT16, "Top"), \ + TraceLoggingInt16(TlgInINT16, "Right"), \ + TraceLoggingInt16(TlgInINT16, "Bottom") + +// We intentionally don't differentiate between A and W versions of CHAR_INFO, because some particularly nasty +// applications smuggle data in the upper bytes of the UnicodeChar field while using the A APIs and then they +// expect to read the same values back at a later time, which is something we stopped supporting. +#define TraceLoggingConsoleCharInfo(value, name) \ + TraceLoggingStruct(2, name), \ + TraceLoggingInt16(value.Char.UnicodeChar, "Char"), \ + TraceLoggingInt16(value.Attributes, "Attributes") class Tracing { diff --git a/src/propsheet/console.h b/src/propsheet/console.h index dfd0efd87e..ef15b4f374 100644 --- a/src/propsheet/console.h +++ b/src/propsheet/console.h @@ -196,13 +196,9 @@ void Undo(HWND hControlWindow); #define DBGFONTS(_params_) #define DBGFONTS2(_params_) - #define DBGCHARS(_params_) - #define DBGOUTPUT(_params_) #else #define DBGFONTS(_params_) #define DBGFONTS2(_params_) - #define DBGCHARS(_params_) - #define DBGOUTPUT(_params_) #endif // clang-format on diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp index 8039977b5e..9009f71c19 100644 --- a/src/server/ApiDispatchers.cpp +++ b/src/server/ApiDispatchers.cpp @@ -34,6 +34,15 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) return p ? p->dwThreadId : 0; } +template +constexpr T saturate(auto val) +{ + constexpr auto min = std::numeric_limits::min(); + constexpr auto max = std::numeric_limits::max(); +#pragma warning(suppress : 4267) // '...': conversion from '...' to 'T', possible loss of data + return val < min ? min : (val > max ? max : val); +} + [[nodiscard]] HRESULT ApiDispatchers::ServerGetConsoleCP(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const /*pbReplyPending*/) { @@ -88,7 +97,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) TraceConsoleAPICallWithOrigin( "SetConsoleMode", TraceLoggingBool(pObjectHandle->IsInputHandle(), "InputHandle"), - TraceLoggingHexULong(a->Mode, "Mode")); + TraceLoggingHexUInt32(a->Mode, "Mode")); if (pObjectHandle->IsInputHandle()) { @@ -115,7 +124,13 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) InputBuffer* pObj; RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_READ, &pObj)); - return m->_pApiRoutines->GetNumberOfConsoleInputEventsImpl(*pObj, a->ReadyEvents); + RETURN_IF_FAILED_EXPECTED(m->_pApiRoutines->GetNumberOfConsoleInputEventsImpl(*pObj, a->ReadyEvents)); + + TraceConsoleAPICallWithOrigin( + "GetNumberOfConsoleInputEvents", + TraceLoggingHexUInt32(a->ReadyEvents, "ReadyEvents")); + + return S_OK; } [[nodiscard]] HRESULT ApiDispatchers::ServerGetConsoleInput(_Inout_ CONSOLE_API_MSG* const m, @@ -135,6 +150,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) // Make sure we have a valid input buffer. const auto pHandleData = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pHandleData); + InputBuffer* pInputBuffer; RETURN_IF_FAILED(pHandleData->GetInputBuffer(GENERIC_READ, &pInputBuffer)); @@ -146,6 +162,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) const auto rgRecords = reinterpret_cast(pvBuffer); const auto cRecords = cbBufferSize / sizeof(INPUT_RECORD); + TraceConsoleAPICallWithOrigin( + "GetConsoleInput", + TraceLoggingHexUInt16(a->Flags, "Flags"), + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingUIntPtr(cRecords, "Records")); + const auto fIsPeek = WI_IsFlagSet(a->Flags, CONSOLE_READ_NOREMOVE); const auto fIsWaitAllowed = WI_IsFlagClear(a->Flags, CONSOLE_READ_NOWAIT); @@ -269,6 +291,14 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) } CATCH_RETURN(); + TraceConsoleAPICallWithOrigin( + "ReadConsole", + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingBoolean(a->ProcessControlZ, "ProcessControlZ"), + TraceLoggingCountedWideString(exeView.data(), saturate(exeView.size()), "ExeName"), + TraceLoggingCountedWideString(initialData.data(), saturate(initialData.size()), "InitialChars"), + TraceLoggingHexUInt32(a->CtrlWakeupMask, "CtrlWakeupMask")); + // ReadConsole needs this to get details associated with an attached process (such as the command history list, telemetry metadata). const auto hConsoleClient = (HANDLE)m->GetProcessHandle(); @@ -408,7 +438,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.FillConsoleOutput; // Capture length of initial fill. - size_t fill = a->Length; + const auto fill = a->Length; // Set written length to 0 in case we early return. a->Length = 0; @@ -425,6 +455,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { case CONSOLE_ATTRIBUTE: { + TraceConsoleAPICallWithOrigin( + "FillConsoleOutputAttribute", + TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), + TraceLoggingUInt32(fill, "Length"), + TraceLoggingHexUInt16(a->Element, "Attribute")); + hr = m->_pApiRoutines->FillConsoleOutputAttributeImpl(*pScreenInfo, a->Element, fill, @@ -435,6 +471,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) case CONSOLE_REAL_UNICODE: case CONSOLE_FALSE_UNICODE: { + TraceConsoleAPICallWithOrigin( + "FillConsoleOutputCharacterW", + TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), + TraceLoggingUInt32(fill, "Length"), + TraceLoggingWChar(a->Element, "Character")); + // GH#3126 if the client application is powershell.exe, then we might // need to enable a compatibility shim. hr = m->_pApiRoutines->FillConsoleOutputCharacterWImpl(*pScreenInfo, @@ -447,6 +489,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) } case CONSOLE_ASCII: { + TraceConsoleAPICallWithOrigin( + "FillConsoleOutputCharacterA", + TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), + TraceLoggingUInt32(fill, "Length"), + TraceLoggingChar(static_cast(a->Element), "Character")); + hr = m->_pApiRoutines->FillConsoleOutputCharacterAImpl(*pScreenInfo, static_cast(a->Element), fill, @@ -466,6 +514,8 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) [[nodiscard]] HRESULT ApiDispatchers::ServerSetConsoleActiveScreenBuffer(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const /*pbReplyPending*/) { + TraceConsoleAPICallWithOrigin("SetConsoleActiveScreenBuffer"); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); @@ -479,6 +529,8 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) [[nodiscard]] HRESULT ApiDispatchers::ServerFlushConsoleInputBuffer(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const /*pbReplyPending*/) { + TraceConsoleAPICallWithOrigin("ServerFlushConsoleInputBuffer"); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); @@ -494,6 +546,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.SetConsoleCP; + TraceConsoleAPICallWithOrigin( + "SetConsoleCP", + TraceLoggingBool(!a->Output, "InputHandle"), + TraceLoggingHexUInt32(a->CodePage, "CodePage")); + if (a->Output) { return m->_pApiRoutines->SetConsoleOutputCodePageImpl(a->CodePage); @@ -518,6 +575,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) auto visible = false; m->_pApiRoutines->GetConsoleCursorInfoImpl(*pObj, a->CursorSize, visible); a->Visible = !!visible; + + TraceConsoleAPICallWithOrigin( + "GetConsoleCursorInfo", + TraceLoggingUInt32(a->CursorSize, "CursorSize"), + TraceLoggingBoolean(a->Visible, "Visible")); + return S_OK; } @@ -526,6 +589,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.SetConsoleCursorInfo; + TraceConsoleAPICallWithOrigin( + "SetConsoleCursorInfo", + TraceLoggingUInt32(a->CursorSize, "CursorSize"), + TraceLoggingBoolean(a->Visible, "Visible")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); @@ -564,6 +632,18 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) a->Attributes = ex.wAttributes; a->PopupAttributes = ex.wPopupAttributes; + TraceConsoleAPICallWithOrigin( + "GetConsoleScreenBufferInfo", + TraceLoggingConsoleCoord(a->Size, "Size"), + TraceLoggingConsoleCoord(a->CursorPosition, "CursorPosition"), + TraceLoggingConsoleCoord(a->ScrollPosition, "ScrollPosition"), + TraceLoggingHexUInt16(a->Attributes, "Attributes"), + TraceLoggingConsoleCoord(a->CurrentWindowSize, "CurrentWindowSize"), + TraceLoggingConsoleCoord(a->MaximumWindowSize, "MaximumWindowSize"), + TraceLoggingHexUInt16(a->PopupAttributes, "PopupAttributes"), + TraceLoggingBoolean(a->FullscreenSupported, "FullscreenSupported"), + TraceLoggingHexULongFixedArray(&a->ColorTable[0], 16, "ColorTable")); + return S_OK; } @@ -572,6 +652,18 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.SetConsoleScreenBufferInfo; + TraceConsoleAPICallWithOrigin( + "SetConsoleScreenBufferInfo", + TraceLoggingConsoleCoord(a->Size, "Size"), + TraceLoggingConsoleCoord(a->CursorPosition, "CursorPosition"), + TraceLoggingConsoleCoord(a->ScrollPosition, "ScrollPosition"), + TraceLoggingHexUInt16(a->Attributes, "Attributes"), + TraceLoggingConsoleCoord(a->CurrentWindowSize, "CurrentWindowSize"), + TraceLoggingConsoleCoord(a->MaximumWindowSize, "MaximumWindowSize"), + TraceLoggingHexUInt16(a->PopupAttributes, "PopupAttributes"), + TraceLoggingBoolean(a->FullscreenSupported, "FullscreenSupported"), + TraceLoggingHexULongFixedArray(&a->ColorTable[0], 16, "ColorTable")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); @@ -594,12 +686,6 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) ex.wAttributes = a->Attributes; ex.wPopupAttributes = a->PopupAttributes; - TraceConsoleAPICallWithOrigin( - "SetConsoleScreenBufferInfoEx", - TraceLoggingConsoleCoord(a->Size, "BufferSize"), - TraceLoggingConsoleCoord(a->CurrentWindowSize, "WindowSize"), - TraceLoggingConsoleCoord(a->MaximumWindowSize, "MaxWindowSize")); - return m->_pApiRoutines->SetConsoleScreenBufferInfoExImpl(*pObj, ex); } @@ -608,16 +694,16 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.SetConsoleScreenBufferSize; + TraceConsoleAPICallWithOrigin( + "SetConsoleScreenBufferSize", + TraceLoggingConsoleCoord(a->Size, "BufferSize")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); SCREEN_INFORMATION* pObj; RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); - TraceConsoleAPICallWithOrigin( - "SetConsoleScreenBufferSize", - TraceLoggingConsoleCoord(a->Size, "BufferSize")); - return m->_pApiRoutines->SetConsoleScreenBufferSizeImpl(*pObj, til::wrap_coord_size(a->Size)); } @@ -626,6 +712,10 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.SetConsoleCursorPosition; + TraceConsoleAPICallWithOrigin( + "SetConsoleCursorPosition", + TraceLoggingConsoleCoord(a->CursorPosition, "CursorPosition")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); @@ -648,7 +738,13 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) auto size = til::wrap_coord_size(a->Size); m->_pApiRoutines->GetLargestConsoleWindowSizeImpl(*pObj, size); - return til::unwrap_coord_size_hr(size, a->Size); + RETURN_IF_FAILED_EXPECTED(til::unwrap_coord_size_hr(size, a->Size)); + + TraceConsoleAPICallWithOrigin( + "GetLargestConsoleWindowSize", + TraceLoggingConsoleCoord(a->Size, "Size")); + + return S_OK; } [[nodiscard]] HRESULT ApiDispatchers::ServerScrollConsoleScreenBuffer(_Inout_ CONSOLE_API_MSG* const m, @@ -656,6 +752,15 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.ScrollConsoleScreenBuffer; + TraceConsoleAPICallWithOrigin( + "ScrollConsoleScreenBuffer", + TraceLoggingConsoleSmallRect(a->ScrollRectangle, "ScrollRectangle"), + TraceLoggingConsoleSmallRect(a->ClipRectangle, "ClipRectangle"), + TraceLoggingBoolean(a->Clip, "Clip"), + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingConsoleCoord(a->DestinationOrigin, "DestinationOrigin"), + TraceLoggingConsoleCharInfo(a->Fill, "Fill")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); @@ -690,16 +795,16 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.SetConsoleTextAttribute; + TraceConsoleAPICallWithOrigin( + "SetConsoleTextAttribute", + TraceLoggingHexUInt16(a->Attributes, "Attributes")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); SCREEN_INFORMATION* pObj; RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); - TraceConsoleAPICallWithOrigin( - "SetConsoleTextAttribute", - TraceLoggingHexUInt16(a->Attributes, "Attributes")); - RETURN_HR(m->_pApiRoutines->SetConsoleTextAttributeImpl(*pObj, a->Attributes)); } @@ -708,20 +813,17 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL2.SetConsoleWindowInfo; + TraceConsoleAPICallWithOrigin( + "SetConsoleWindowInfo", + TraceLoggingBool(a->Absolute, "IsWindowRectAbsolute"), + TraceLoggingConsoleSmallRect(a->Window, "Window")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); SCREEN_INFORMATION* pObj; RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); - TraceConsoleAPICallWithOrigin( - "SetConsoleWindowInfo", - TraceLoggingBool(a->Absolute, "IsWindowRectAbsolute"), - TraceLoggingInt32(a->Window.Left, "WindowRectLeft"), - TraceLoggingInt32(a->Window.Right, "WindowRectRight"), - TraceLoggingInt32(a->Window.Top, "WindowRectTop"), - TraceLoggingInt32(a->Window.Bottom, "WindowRectBottom")); - return m->_pApiRoutines->SetConsoleWindowInfoImpl(*pObj, a->Absolute, til::wrap_small_rect(a->Window)); } @@ -749,6 +851,10 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) case CONSOLE_ATTRIBUTE: { const std::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(WORD)); + TraceConsoleAPICallWithOrigin( + "ReadConsoleOutputAttribute", + TraceLoggingConsoleCoord(a->ReadCoord, "ReadCoord"), + TraceLoggingUIntPtr(buffer.size(), "Records")); RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputAttributeImpl(*pScreenInfo, til::wrap_coord(a->ReadCoord), buffer, written)); break; } @@ -756,12 +862,20 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) case CONSOLE_FALSE_UNICODE: { const std::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(wchar_t)); + TraceConsoleAPICallWithOrigin( + "ReadConsoleOutputCharacterW", + TraceLoggingConsoleCoord(a->ReadCoord, "ReadCoord"), + TraceLoggingUIntPtr(buffer.size(), "Records")); RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputCharacterWImpl(*pScreenInfo, til::wrap_coord(a->ReadCoord), buffer, written)); break; } case CONSOLE_ASCII: { const std::span buffer(reinterpret_cast(pvBuffer), cbBuffer); + TraceConsoleAPICallWithOrigin( + "ReadConsoleOutputCharacterA", + TraceLoggingConsoleCoord(a->ReadCoord, "ReadCoord"), + TraceLoggingUIntPtr(buffer.size(), "Records")); RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputCharacterAImpl(*pScreenInfo, til::wrap_coord(a->ReadCoord), buffer, written)); break; } @@ -798,6 +912,13 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t written; std::span buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(INPUT_RECORD)); + + TraceConsoleAPICallWithOrigin( + "WriteConsoleInput", + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingBoolean(a->Append, "Append"), + TraceLoggingUIntPtr(buffer.size(), "Records")); + if (!a->Unicode) { RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleInputAImpl(*pInputBuffer, buffer, written, !!a->Append)); @@ -841,6 +962,13 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) RETURN_HR_IF(E_INVALIDARG, cbSize < regionBytes); // If given fewer bytes on input than we need to do this write, it's invalid. const std::span buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(CHAR_INFO)); + + TraceConsoleAPICallWithOrigin( + "WriteConsoleOutput", + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingConsoleSmallRect(a->CharRegion, "CharRegion"), + TraceLoggingUIntPtr(buffer.size(), "Records")); + if (!a->Unicode) { RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleOutputAImpl(*pScreenInfo, buffer, originalRegion, writtenRegion)); @@ -883,7 +1011,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) TraceConsoleAPICallWithOrigin( "WriteConsoleOutputCharacterA", TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), - TraceLoggingUInt32(a->NumRecords, "NumRecords")); + TraceLoggingCountedString(text.data(), saturate(text.size()), "Buffer")); hr = m->_pApiRoutines->WriteConsoleOutputCharacterAImpl(*pScreenInfo, text, @@ -900,7 +1028,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) TraceConsoleAPICallWithOrigin( "WriteConsoleOutputCharacterW", TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), - TraceLoggingUInt32(a->NumRecords, "NumRecords")); + TraceLoggingCountedWideString(text.data(), saturate(text.size()), "Buffer")); hr = m->_pApiRoutines->WriteConsoleOutputCharacterWImpl(*pScreenInfo, text, @@ -916,7 +1044,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) TraceConsoleAPICallWithOrigin( "WriteConsoleOutputAttribute", TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), - TraceLoggingUInt32(a->NumRecords, "NumRecords")); + TraceLoggingHexUInt16Array(text.data(), saturate(text.size()), "Buffer")); hr = m->_pApiRoutines->WriteConsoleOutputAttributeImpl(*pScreenInfo, text, @@ -965,6 +1093,13 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) RETURN_HR_IF(E_INVALIDARG, regionArea > 0 && ((regionArea > ULONG_MAX / sizeof(CHAR_INFO)) || (cbBuffer < regionBytes))); std::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(CHAR_INFO)); + + TraceConsoleAPICallWithOrigin( + "ReadConsoleOutput", + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingConsoleSmallRect(a->CharRegion, "CharRegion"), + TraceLoggingUIntPtr(buffer.size(), "Records")); + auto finalRegion = Microsoft::Console::Types::Viewport::Empty(); // the actual region read out of the buffer if (!a->Unicode) { @@ -1016,6 +1151,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) LOG_IF_FAILED(m->_pApiRoutines->GetConsoleTitleWImpl(buffer, written, needed)); } + TraceConsoleAPICallWithOrigin( + "GetConsoleTitleW", + TraceLoggingBoolean(a->Original, "Original"), + TraceLoggingCountedWideString(buffer.data(), saturate(written), "Buffer")); + // We must return the needed length of the title string in the TitleLength. LOG_IF_FAILED(SizeTToULong(needed, &a->TitleLength)); @@ -1036,6 +1176,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) hr = m->_pApiRoutines->GetConsoleTitleAImpl(buffer, written, needed); } + TraceConsoleAPICallWithOrigin( + "GetConsoleTitleA", + TraceLoggingBoolean(a->Original, "Original"), + TraceLoggingCountedString(buffer.data(), saturate(written), "Buffer")); + // We must return the needed length of the title string in the TitleLength. LOG_IF_FAILED(SizeTToULong(needed, &a->TitleLength)); @@ -1059,11 +1204,21 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) if (a->Unicode) { const std::wstring_view title(reinterpret_cast(pvBuffer), cbOriginalLength / sizeof(wchar_t)); + + TraceConsoleAPICallWithOrigin( + "SetConsoleTitleW", + TraceLoggingCountedWideString(title.data(), saturate(title.size()), "Buffer")); + return m->_pApiRoutines->SetConsoleTitleWImpl(title); } else { const std::string_view title(reinterpret_cast(pvBuffer), cbOriginalLength); + + TraceConsoleAPICallWithOrigin( + "SetConsoleTitleA", + TraceLoggingCountedString(title.data(), saturate(title.size()), "Buffer")); + return m->_pApiRoutines->SetConsoleTitleAImpl(title); } } @@ -1074,6 +1229,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) const auto a = &m->u.consoleMsgL3.GetConsoleMouseInfo; m->_pApiRoutines->GetNumberOfConsoleMouseButtonsImpl(a->NumButtons); + + TraceConsoleAPICallWithOrigin( + "GetConsoleMouseInfo", + TraceLoggingUInt32(a->NumButtons, "NumButtons")); + return S_OK; } @@ -1090,7 +1250,14 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) auto size = til::wrap_coord_size(a->FontSize); RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleFontSizeImpl(*pObj, a->FontIndex, size)); - return til::unwrap_coord_size_hr(size, a->FontSize); + RETURN_IF_FAILED_EXPECTED(til::unwrap_coord_size_hr(size, a->FontSize)); + + TraceConsoleAPICallWithOrigin( + "GetConsoleFontSize", + TraceLoggingUInt32(a->FontIndex, "FontIndex"), + TraceLoggingConsoleCoord(a->FontSize, "FontSize")); + + return S_OK; } [[nodiscard]] HRESULT ApiDispatchers::ServerGetConsoleCurrentFont(_Inout_ CONSOLE_API_MSG* const m, @@ -1115,6 +1282,15 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) a->FontSize = FontInfo.dwFontSize; a->FontWeight = FontInfo.FontWeight; + TraceConsoleAPICallWithOrigin( + "GetConsoleFontSize", + TraceLoggingBoolean(a->MaximumWindow, "MaximumWindow"), + TraceLoggingUInt32(a->FontIndex, "FontIndex"), + TraceLoggingConsoleCoord(a->FontSize, "FontSize"), + TraceLoggingUInt32(a->FontFamily, "FontFamily"), + TraceLoggingUInt32(a->FontWeight, "FontWeight"), + TraceLoggingWideString(&a->FaceName[0], "FaceName")); + return S_OK; } @@ -1123,6 +1299,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL3.SetConsoleDisplayMode; + TraceConsoleAPICallWithOrigin( + "SetConsoleDisplayMode", + TraceLoggingHexUInt32(a->dwFlags, "dwFlags"), + TraceLoggingConsoleCoord(a->ScreenBufferDimensions, "ScreenBufferDimensions")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); @@ -1131,7 +1312,9 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) auto size = til::wrap_coord_size(a->ScreenBufferDimensions); RETURN_IF_FAILED(m->_pApiRoutines->SetConsoleDisplayModeImpl(*pObj, a->dwFlags, size)); - return til::unwrap_coord_size_hr(size, a->ScreenBufferDimensions); + RETURN_IF_FAILED_EXPECTED(til::unwrap_coord_size_hr(size, a->ScreenBufferDimensions)); + + return S_OK; } [[nodiscard]] HRESULT ApiDispatchers::ServerGetConsoleDisplayMode(_Inout_ CONSOLE_API_MSG* const m, @@ -1142,6 +1325,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) // Historically this has never checked the handles. It just returns global state. m->_pApiRoutines->GetConsoleDisplayModeImpl(a->ModeFlags); + + TraceConsoleAPICallWithOrigin( + "GetConsoleDisplayMode", + TraceLoggingHexUInt32(a->ModeFlags, "ModeFlags")); + return S_OK; } @@ -1180,6 +1368,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) const std::wstring_view inputSource(reinterpret_cast(pvInputSource), cbInputSource / sizeof(wchar_t)); const std::wstring_view inputTarget(reinterpret_cast(pvInputTarget), cbInputTarget / sizeof(wchar_t)); + TraceConsoleAPICallWithOrigin( + "AddConsoleAliasW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedWideString(inputSource.data(), saturate(inputSource.size()), "Source"), + TraceLoggingCountedWideString(inputTarget.data(), saturate(inputTarget.size()), "Target")); + return m->_pApiRoutines->AddConsoleAliasWImpl(inputSource, inputTarget, inputExeName); } else @@ -1188,6 +1382,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) const std::string_view inputSource(pvInputSource, cbInputSource); const std::string_view inputTarget(pvInputTarget, cbInputTarget); + TraceConsoleAPICallWithOrigin( + "AddConsoleAliasA", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedString(inputSource.data(), saturate(inputSource.size()), "Source"), + TraceLoggingCountedString(inputTarget.data(), saturate(inputTarget.size()), "Target")); + return m->_pApiRoutines->AddConsoleAliasAImpl(inputSource, inputTarget, inputExeName); } } @@ -1233,6 +1433,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) hr = m->_pApiRoutines->GetConsoleAliasWImpl(inputSource, outputBuffer, cchWritten, inputExeName); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedWideString(inputSource.data(), saturate(inputSource.size()), "Source"), + TraceLoggingCountedWideString(outputBuffer.data(), saturate(cchWritten), "Output")); + // We must set the reply length in bytes. Convert back from characters. RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); } @@ -1245,6 +1451,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) hr = m->_pApiRoutines->GetConsoleAliasAImpl(inputSource, outputBuffer, cchWritten, inputExeName); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasW", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedString(inputSource.data(), saturate(inputSource.size()), "Source"), + TraceLoggingCountedString(outputBuffer.data(), saturate(cchWritten), "Output")); + cbWritten = cchWritten; } @@ -1281,6 +1493,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t cchAliasesLength; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesLengthWImpl(inputExeName, cchAliasesLength)); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasesLengthW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingUIntPtr(cchAliasesLength, "Length")); + RETURN_IF_FAILED(SizeTMult(cchAliasesLength, sizeof(wchar_t), &cbAliasesLength)); } else @@ -1289,6 +1506,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t cchAliasesLength; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesLengthAImpl(inputExeName, cchAliasesLength)); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasesLengthA", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingUIntPtr(cchAliasesLength, "Length")); + cbAliasesLength = cchAliasesLength; } @@ -1307,12 +1529,22 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { size_t cchAliasExesLength; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesLengthWImpl(cchAliasExesLength)); + + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasExesLengthW", + TraceLoggingUIntPtr(cchAliasExesLength, "Length")); + cbAliasExesLength = cchAliasExesLength * sizeof(wchar_t); } else { size_t cchAliasExesLength; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesLengthAImpl(cchAliasExesLength)); + + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasExesLengthA", + TraceLoggingUIntPtr(cchAliasExesLength, "Length")); + cbAliasExesLength = cchAliasExesLength; } @@ -1343,6 +1575,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesWImpl(inputExeName, outputBuffer, cchWritten)); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasesW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedWideString(outputBuffer.data(), saturate(cchWritten), "Output")); + // We must set the reply length in bytes. Convert back from characters. RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); } @@ -1354,6 +1591,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesAImpl(inputExeName, outputBuffer, cchWritten)); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasesA", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedString(outputBuffer.data(), saturate(cchWritten), "Output")); + cbWritten = cchWritten; } @@ -1380,6 +1622,10 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesWImpl(outputBuffer, cchWritten)); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasExesW", + TraceLoggingCountedWideString(outputBuffer.data(), saturate(cchWritten), "Output")); + RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); } else @@ -1388,6 +1634,10 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesAImpl(outputBuffer, cchWritten)); + TraceConsoleAPICallWithOrigin( + "GetConsoleAliasExesA", + TraceLoggingCountedString(outputBuffer.data(), saturate(cchWritten), "Output")); + cbWritten = cchWritten; } @@ -1412,12 +1662,20 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + TraceConsoleAPICallWithOrigin( + "ExpungeConsoleCommandHistoryW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName")); + return m->_pApiRoutines->ExpungeConsoleCommandHistoryWImpl(inputExeName); } else { const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + TraceConsoleAPICallWithOrigin( + "ExpungeConsoleCommandHistoryA", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName")); + return m->_pApiRoutines->ExpungeConsoleCommandHistoryAImpl(inputExeName); } } @@ -1426,6 +1684,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) _Inout_ BOOL* const /*pbReplyPending*/) { const auto a = &m->u.consoleMsgL3.SetConsoleNumberOfCommandsW; + PVOID pvExeName; ULONG cbExeNameLength; RETURN_IF_FAILED(m->GetInputBuffer(&pvExeName, &cbExeNameLength)); @@ -1435,12 +1694,22 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + TraceConsoleAPICallWithOrigin( + "SetConsoleNumberOfCommandsW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingUInt32(a->NumCommands, "NumCommands")); + return m->_pApiRoutines->SetConsoleNumberOfCommandsWImpl(inputExeName, NumberOfCommands); } else { const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + TraceConsoleAPICallWithOrigin( + "SetConsoleNumberOfCommandsA", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingUInt32(a->NumCommands, "NumCommands")); + return m->_pApiRoutines->SetConsoleNumberOfCommandsAImpl(inputExeName, NumberOfCommands); } } @@ -1462,6 +1731,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryLengthWImpl(inputExeName, cchCommandHistoryLength)); + TraceConsoleAPICallWithOrigin( + "GetConsoleCommandHistoryLengthW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingUIntPtr(cchCommandHistoryLength, "CommandHistoryLength")); + // We must set the reply length in bytes. Convert back from characters. RETURN_IF_FAILED(SizeTMult(cchCommandHistoryLength, sizeof(wchar_t), &cbCommandHistoryLength)); } @@ -1472,6 +1746,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryLengthAImpl(inputExeName, cchCommandHistoryLength)); + TraceConsoleAPICallWithOrigin( + "GetConsoleCommandHistoryLengthA", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingUIntPtr(cchCommandHistoryLength, "CommandHistoryLength")); + cbCommandHistoryLength = cchCommandHistoryLength; } @@ -1502,6 +1781,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryWImpl(inputExeName, outputBuffer, cchWritten)); + TraceConsoleAPICallWithOrigin( + "GetConsoleCommandHistoryW", + TraceLoggingCountedWideString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedWideString(outputBuffer.data(), saturate(cchWritten), "Output")); + // We must set the reply length in bytes. Convert back from characters. RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); } @@ -1512,6 +1796,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryAImpl(inputExeName, outputBuffer, cchWritten)); + TraceConsoleAPICallWithOrigin( + "GetConsoleCommandHistory", + TraceLoggingCountedString(inputExeName.data(), saturate(inputExeName.size()), "ExeName"), + TraceLoggingCountedString(outputBuffer.data(), saturate(cchWritten), "Output")); + cbWritten = cchWritten; } @@ -1529,6 +1818,11 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) const auto a = &m->u.consoleMsgL3.GetConsoleWindow; m->_pApiRoutines->GetConsoleWindowImpl(a->hwnd); + + TraceConsoleAPICallWithOrigin( + "GetConsoleWindow", + TraceLoggingPointer(a->hwnd, "hwnd")); + return S_OK; } @@ -1538,6 +1832,14 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) const auto a = &m->u.consoleMsgL3.GetConsoleSelectionInfo; m->_pApiRoutines->GetConsoleSelectionInfoImpl(a->SelectionInfo); + + TraceConsoleAPICallWithOrigin( + "GetConsoleSelectionInfo", + TraceLoggingStruct(3, "SelectionInfo"), + TraceLoggingUInt32(a->SelectionInfo.dwFlags, "dwFlags"), + TraceLoggingConsoleCoord(a->SelectionInfo.dwSelectionAnchor, "dwSelectionAnchor"), + TraceLoggingConsoleSmallRect(a->SelectionInfo.srSelection, "srSelection")); + return S_OK; } @@ -1555,6 +1857,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) a->HistoryBufferSize = info.HistoryBufferSize; a->NumberOfHistoryBuffers = info.NumberOfHistoryBuffers; + TraceConsoleAPICallWithOrigin( + "GetConsoleHistory", + TraceLoggingUInt32(a->HistoryBufferSize, "HistoryBufferSize"), + TraceLoggingUInt32(a->NumberOfHistoryBuffers, "NumberOfHistoryBuffers"), + TraceLoggingUInt32(a->dwFlags, "dwFlags")); + return S_OK; } @@ -1563,6 +1871,12 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL3.SetConsoleHistory; + TraceConsoleAPICallWithOrigin( + "SetConsoleHistory", + TraceLoggingUInt32(a->HistoryBufferSize, "HistoryBufferSize"), + TraceLoggingUInt32(a->NumberOfHistoryBuffers, "NumberOfHistoryBuffers"), + TraceLoggingUInt32(a->dwFlags, "dwFlags")); + CONSOLE_HISTORY_INFO info; info.cbSize = sizeof(info); info.dwFlags = a->dwFlags; @@ -1577,6 +1891,15 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) { const auto a = &m->u.consoleMsgL3.SetCurrentConsoleFont; + TraceConsoleAPICallWithOrigin( + "SetCurrentConsoleFont", + TraceLoggingBoolean(a->MaximumWindow, "MaximumWindow"), + TraceLoggingUInt32(a->FontIndex, "FontIndex"), + TraceLoggingConsoleCoord(a->FontSize, "FontSize"), + TraceLoggingUInt32(a->FontFamily, "FontFamily"), + TraceLoggingUInt32(a->FontWeight, "FontWeight"), + TraceLoggingWideString(&a->FaceName[0], "FaceName")); + const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); From 99061ee272b72c6d565802068f7b09ab12e4173e Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 23 Apr 2024 02:07:00 +0200 Subject: [PATCH 09/53] Use float instead of double by default (#17100) While `double` is probably generally preferable for UI code, our application is essentially a complex wrapper wrapper around DWrite, D2D and D3D, all of which use `float` exclusively. Of course it also uses XAML, but that one uses `float` for roughly 1/3rd of its API functions, so I'm not sure what it prefers. Additionally, it's mostly a coincidence that we use WinUI/XAML for Windows Terminal whereas DWrite/D2D/D3D are effectively essential. This is demonstrated by the fact that we have a `HwndTerminal`, while there's no alternative to e.g. D3D on Windows. The goal of this PR is that DIP based calculations never end up mixing `float` and `double`. This PR also changes opacity-related values to `float` because I felt like that fits the theme. --- .../TerminalApp/ActionPreviewHandlers.cpp | 2 +- .../TerminalApp/AppActionHandlers.cpp | 4 +- src/cascadia/TerminalApp/Pane.cpp | 12 ++-- src/cascadia/TerminalApp/TerminalPage.cpp | 8 +-- src/cascadia/TerminalApp/TitlebarControl.cpp | 6 +- src/cascadia/TerminalApp/TitlebarControl.h | 2 +- src/cascadia/TerminalApp/TitlebarControl.idl | 2 +- src/cascadia/TerminalControl/ControlCore.cpp | 24 +++---- src/cascadia/TerminalControl/ControlCore.h | 12 ++-- src/cascadia/TerminalControl/ControlCore.idl | 4 +- .../TerminalControl/ControlInteractivity.cpp | 16 ++--- .../TerminalControl/ControlInteractivity.h | 6 +- .../TerminalControl/ControlInteractivity.idl | 2 +- src/cascadia/TerminalControl/EventArgs.h | 4 +- src/cascadia/TerminalControl/EventArgs.idl | 2 +- src/cascadia/TerminalControl/HwndTerminal.cpp | 4 +- src/cascadia/TerminalControl/HwndTerminal.hpp | 2 +- .../TerminalControl/IControlAppearance.idl | 4 +- .../InteractivityAutomationPeer.cpp | 6 +- .../InteractivityAutomationPeer.h | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 64 ++++++++++--------- src/cascadia/TerminalControl/TermControl.h | 4 +- src/cascadia/TerminalControl/TermControl.idl | 4 +- src/cascadia/TerminalCore/ICoreAppearance.idl | 8 +-- .../TerminalSettingsEditor/Appearances.cpp | 2 +- .../TerminalSettingsEditor/Appearances.idl | 2 +- .../TerminalSettingsEditor/ProfileViewModel.h | 2 +- .../ProfileViewModel.idl | 2 +- .../TerminalSettingsModel/ActionArgs.h | 6 +- .../TerminalSettingsModel/ActionArgs.idl | 6 +- .../AppearanceConfig.cpp | 4 +- .../TerminalSettingsModel/AppearanceConfig.h | 2 +- .../IAppearanceConfig.idl | 4 +- .../TerminalSettingsModel/JsonUtils.h | 4 +- .../TerminalSettingsModel/MTSMSettings.h | 2 +- .../TerminalSettingsModel/TerminalSettings.h | 4 +- .../TerminalSettingsSerializationHelpers.h | 10 +-- .../UnitTests_Control/ControlCoreTests.cpp | 56 ++++++++-------- .../ControlInteractivityTests.cpp | 18 +++--- .../UnitTests_SettingsModel/CommandTests.cpp | 18 +++--- src/cascadia/WindowsTerminal/IslandWindow.cpp | 2 +- src/cascadia/inc/ControlProperties.h | 4 +- src/types/IControlAccessibilityInfo.h | 2 +- src/types/TermControlUiaProvider.cpp | 2 +- src/types/TermControlUiaProvider.hpp | 2 +- 45 files changed, 180 insertions(+), 178 deletions(-) diff --git a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp index 67212bbbe4..03e45ea3f9 100644 --- a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp +++ b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp @@ -127,7 +127,7 @@ namespace winrt::TerminalApp::implementation auto originalOpacity{ control.BackgroundOpacity() }; // Apply the new opacity - control.AdjustOpacity(args.Opacity() / 100.0, args.Relative()); + control.AdjustOpacity(args.Opacity() / 100.0f, args.Relative()); if (backup) { diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 2fd6680574..d044cbe13f 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -286,7 +286,7 @@ namespace winrt::TerminalApp::implementation _SplitPane(terminalTab, realArgs.SplitDirection(), // This is safe, we're already filtering so the value is (0, 1) - ::base::saturated_cast(realArgs.SplitSize()), + realArgs.SplitSize(), _MakePane(realArgs.ContentArgs(), duplicateFromTab)); args.Handled(true); } @@ -1247,7 +1247,7 @@ namespace winrt::TerminalApp::implementation if (const auto& realArgs = args.ActionArgs().try_as()) { const auto res = _ApplyToActiveControls([&](auto& control) { - control.AdjustOpacity(realArgs.Opacity() / 100.0, realArgs.Relative()); + control.AdjustOpacity(realArgs.Opacity() / 100.0f, realArgs.Relative()); }); args.Handled(res); } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 7d11f4edc6..ac59961235 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -151,7 +151,7 @@ Pane::BuildStartupState Pane::BuildStartupActions(uint32_t currentId, uint32_t n // When creating a pane the split size is the size of the new pane // and not position. const auto splitDirection = _splitState == SplitState::Horizontal ? SplitDirection::Down : SplitDirection::Right; - const auto splitSize = (kind != BuildStartupKind::None && _IsLeaf() ? .5 : 1. - _desiredSplitPosition); + const auto splitSize = (kind != BuildStartupKind::None && _IsLeaf() ? 0.5f : 1.0f - _desiredSplitPosition); SplitPaneArgs args{ SplitType::Manual, splitDirection, splitSize, terminalArgs }; actionAndArgs.Args(args); @@ -1595,12 +1595,12 @@ void Pane::_CloseChildRoutine(const bool closeFirst) const auto splitWidth = _splitState == SplitState::Vertical; Size removedOriginalSize{ - ::base::saturated_cast(removedChild->_root.ActualWidth()), - ::base::saturated_cast(removedChild->_root.ActualHeight()) + static_cast(removedChild->_root.ActualWidth()), + static_cast(removedChild->_root.ActualHeight()) }; Size remainingOriginalSize{ - ::base::saturated_cast(remainingChild->_root.ActualWidth()), - ::base::saturated_cast(remainingChild->_root.ActualHeight()) + static_cast(remainingChild->_root.ActualWidth()), + static_cast(remainingChild->_root.ActualHeight()) }; // Remove both children from the grid @@ -1902,7 +1902,7 @@ void Pane::_SetupEntranceAnimation() // looks bad. _secondChild->_root.Background(_themeResources.unfocusedBorderBrush); - const auto [firstSize, secondSize] = _CalcChildrenSizes(::base::saturated_cast(totalSize)); + const auto [firstSize, secondSize] = _CalcChildrenSizes(static_cast(totalSize)); // This is safe to capture this, because it's only being called in the // context of this method (not on another thread) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 1749acce26..b10e2157c1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1947,8 +1947,8 @@ namespace winrt::TerminalApp::implementation layout.LaunchMode({ mode }); // Only save the content size because the tab size will be added on load. - const auto contentWidth = ::base::saturated_cast(_tabContent.ActualWidth()); - const auto contentHeight = ::base::saturated_cast(_tabContent.ActualHeight()); + const auto contentWidth = static_cast(_tabContent.ActualWidth()); + const auto contentHeight = static_cast(_tabContent.ActualHeight()); const winrt::Windows::Foundation::Size windowSize{ contentWidth, contentHeight }; layout.InitialSize(windowSize); @@ -2358,8 +2358,8 @@ namespace winrt::TerminalApp::implementation { return; } - const auto contentWidth = ::base::saturated_cast(_tabContent.ActualWidth()); - const auto contentHeight = ::base::saturated_cast(_tabContent.ActualHeight()); + const auto contentWidth = static_cast(_tabContent.ActualWidth()); + const auto contentHeight = static_cast(_tabContent.ActualHeight()); const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; const auto realSplitType = activeTab->PreCalculateCanSplit(splitDirection, splitSize, availableSpace); diff --git a/src/cascadia/TerminalApp/TitlebarControl.cpp b/src/cascadia/TerminalApp/TitlebarControl.cpp index bd5748eb27..76ff3cc7d6 100644 --- a/src/cascadia/TerminalApp/TitlebarControl.cpp +++ b/src/cascadia/TerminalApp/TitlebarControl.cpp @@ -44,12 +44,12 @@ namespace winrt::TerminalApp::implementation }); } - double TitlebarControl::CaptionButtonWidth() + float TitlebarControl::CaptionButtonWidth() { // Divide by three, since we know there are only three buttons. When // Windows 12 comes along and adds another, we can update this /s - static auto width{ MinMaxCloseControl().ActualWidth() / 3.0 }; - return width; + const auto minMaxCloseWidth = MinMaxCloseControl().ActualWidth(); + return static_cast(minMaxCloseWidth) / 3.0f; } IInspectable TitlebarControl::Content() diff --git a/src/cascadia/TerminalApp/TitlebarControl.h b/src/cascadia/TerminalApp/TitlebarControl.h index 259361302a..4dee4fb730 100644 --- a/src/cascadia/TerminalApp/TitlebarControl.h +++ b/src/cascadia/TerminalApp/TitlebarControl.h @@ -15,7 +15,7 @@ namespace winrt::TerminalApp::implementation void PressButton(CaptionButton button); winrt::fire_and_forget ClickButton(CaptionButton button); void ReleaseButtons(); - double CaptionButtonWidth(); + float CaptionButtonWidth(); IInspectable Content(); void Content(IInspectable content); diff --git a/src/cascadia/TerminalApp/TitlebarControl.idl b/src/cascadia/TerminalApp/TitlebarControl.idl index 1227effd97..b7a06ba987 100644 --- a/src/cascadia/TerminalApp/TitlebarControl.idl +++ b/src/cascadia/TerminalApp/TitlebarControl.idl @@ -27,7 +27,7 @@ namespace TerminalApp void PressButton(CaptionButton button); void ClickButton(CaptionButton button); void ReleaseButtons(); - Double CaptionButtonWidth { get; }; + Single CaptionButtonWidth { get; }; IInspectable Content; Windows.UI.Xaml.Controls.Border DragBar { get; }; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 5531ca25cb..cfba0a1d46 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -677,7 +677,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - void ControlCore::AdjustOpacity(const double adjustment) + void ControlCore::AdjustOpacity(const float adjustment) { if (adjustment == 0) { @@ -694,11 +694,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - focused (default == true): Whether the window is focused or unfocused. // Return Value: // - - void ControlCore::_setOpacity(const double opacity, bool focused) + void ControlCore::_setOpacity(const float opacity, bool focused) { - const auto newOpacity = std::clamp(opacity, - 0.0, - 1.0); + const auto newOpacity = std::clamp(opacity, 0.0f, 1.0f); if (newOpacity == Opacity()) { @@ -713,7 +711,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _runtimeFocusedOpacity = focused ? newOpacity : _runtimeFocusedOpacity; // Manually turn off acrylic if they turn off transparency. - _runtimeUseAcrylic = newOpacity < 1.0 && _settings->UseAcrylic(); + _runtimeUseAcrylic = newOpacity < 1.0f && _settings->UseAcrylic(); // Update the renderer as well. It might need to fall back from // cleartype -> grayscale if the BG is transparent / acrylic. @@ -881,7 +879,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Method Description: // - Updates the appearance of the current terminal. // - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal. - void ControlCore::ApplyAppearance(const bool& focused) + void ControlCore::ApplyAppearance(const bool focused) { const auto lock = _terminal->LockForWriting(); const auto& newAppearance{ focused ? _settings->FocusedAppearance() : _settings->UnfocusedAppearance() }; @@ -900,10 +898,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Incase EnableUnfocusedAcrylic is disabled and Focused Acrylic is set to true, // the terminal should ignore the unfocused opacity from settings. // The Focused Opacity from settings should be ignored if overridden at runtime. - bool useFocusedRuntimeOpacity = focused || (!_settings->EnableUnfocusedAcrylic() && UseAcrylic()); - double newOpacity = useFocusedRuntimeOpacity ? - FocusedOpacity() : - newAppearance->Opacity(); + const auto useFocusedRuntimeOpacity = focused || (!_settings->EnableUnfocusedAcrylic() && UseAcrylic()); + const auto newOpacity = useFocusedRuntimeOpacity ? FocusedOpacity() : newAppearance->Opacity(); _setOpacity(newOpacity, focused); // No need to update Acrylic if UnfocusedAcrylic is disabled @@ -1422,8 +1418,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { const auto fontSize = _actualFont.GetSize(); return { - ::base::saturated_cast(fontSize.width), - ::base::saturated_cast(fontSize.height) + static_cast(fontSize.width), + static_cast(fontSize.height) }; } @@ -2350,7 +2346,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _settings->HasUnfocusedAppearance(); } - void ControlCore::AdjustOpacity(const double opacityAdjust, const bool relative) + void ControlCore::AdjustOpacity(const float opacityAdjust, const bool relative) { if (relative) { diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 8748a52ee5..ee2e1e5a84 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -91,7 +91,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void Detach(); void UpdateSettings(const Control::IControlSettings& settings, const IControlAppearance& newAppearance); - void ApplyAppearance(const bool& focused); + void ApplyAppearance(const bool focused); Control::IControlSettings Settings(); Control::IControlAppearance FocusedAppearance() const; Control::IControlAppearance UnfocusedAppearance() const; @@ -136,7 +136,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void LostFocus(); void ToggleShaderEffects(); - void AdjustOpacity(const double adjustment); + void AdjustOpacity(const float adjustment); void ResumeRendering(); void SetHoveredCell(Core::Point terminalPosition); @@ -243,7 +243,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring ReadEntireBuffer() const; Control::CommandHistoryContext CommandHistory() const; - void AdjustOpacity(const double opacity, const bool relative); + void AdjustOpacity(const float opacity, const bool relative); void WindowVisibilityChanged(const bool showOrHide); @@ -258,8 +258,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ShouldShowSelectCommand(); bool ShouldShowSelectOutput(); - RUNTIME_SETTING(double, Opacity, _settings->Opacity()); - RUNTIME_SETTING(double, FocusedOpacity, FocusedAppearance().Opacity()); + RUNTIME_SETTING(float, Opacity, _settings->Opacity()); + RUNTIME_SETTING(float, FocusedOpacity, FocusedAppearance().Opacity()); RUNTIME_SETTING(bool, UseAcrylic, _settings->UseAcrylic()); // -------------------------------- WinRT Events --------------------------------- @@ -399,7 +399,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _updateAntiAliasingMode(); void _connectionOutputHandler(const hstring& hstr); void _updateHoveredCell(const std::optional terminalPosition); - void _setOpacity(const double opacity, const bool focused = true); + void _setOpacity(const float opacity, const bool focused = true); bool _isBackgroundTransparent(); void _focusChanged(bool focused); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index d3f7e91962..038f21c0f4 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -88,7 +88,7 @@ namespace Microsoft.Terminal.Control Windows.Foundation.Size FontSize { get; }; UInt16 FontWeight { get; }; - Double Opacity { get; }; + Single Opacity { get; }; Boolean UseAcrylic { get; }; Boolean TryMarkModeKeybinding(Int16 vkey, @@ -149,7 +149,7 @@ namespace Microsoft.Terminal.Control String ReadEntireBuffer(); CommandHistoryContext CommandHistory(); - void AdjustOpacity(Double Opacity, Boolean relative); + void AdjustOpacity(Single Opacity, Boolean relative); void WindowVisibilityChanged(Boolean showOrHide); void ColorSelection(SelectionColor fg, SelectionColor bg, Microsoft.Terminal.Core.MatchMode matchMode); diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index cb55cca231..7b59babd2e 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -518,7 +518,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlInteractivity::_mouseTransparencyHandler(const int32_t mouseDelta) const { // Transparency is on a scale of [0.0,1.0], so only increment by .01. - const auto effectiveDelta = mouseDelta < 0 ? -.01 : .01; + const auto effectiveDelta = mouseDelta < 0 ? -.01f : .01f; _core->AdjustOpacity(effectiveDelta); } @@ -554,7 +554,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // underneath us. We wouldn't know - we don't want the overhead of // another ScrollPositionChanged handler. If the scrollbar should be // somewhere other than where it is currently, then start from that row. - const auto currentInternalRow = ::base::saturated_cast(::std::round(_internalScrollbarPosition)); + const auto currentInternalRow = std::lround(_internalScrollbarPosition); const auto currentCoreRow = _core->ScrollOffset(); const auto currentOffset = currentInternalRow == currentCoreRow ? _internalScrollbarPosition : @@ -564,13 +564,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation // However, for us, the signs are flipped. // With one of the precision mice, one click is always a multiple of 120 (WHEEL_DELTA), // but the "smooth scrolling" mode results in non-int values - const auto rowDelta = mouseDelta / (-1.0 * WHEEL_DELTA); + const auto rowDelta = mouseDelta / (-1.0f * WHEEL_DELTA); // WHEEL_PAGESCROLL is a Win32 constant that represents the "scroll one page // at a time" setting. If we ignore it, we will scroll a truly absurd number // of rows. - const auto rowsToScroll{ _rowsToScroll == WHEEL_PAGESCROLL ? ::base::saturated_cast(_core->ViewHeight()) : _rowsToScroll }; - auto newValue = (rowsToScroll * rowDelta) + (currentOffset); + const auto rowsToScroll{ _rowsToScroll == WHEEL_PAGESCROLL ? _core->ViewHeight() : _rowsToScroll }; + const auto newValue = rowsToScroll * rowDelta + currentOffset; // Update the Core's viewport position, and raise a // ScrollPositionChanged event to update the scrollbar @@ -600,17 +600,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - newValue: The new top of the viewport // Return Value: // - - void ControlInteractivity::UpdateScrollbar(const double newValue) + void ControlInteractivity::UpdateScrollbar(const float newValue) { // Set this as the new value of our internal scrollbar representation. // We're doing this so we can accumulate fractional amounts of a row to // scroll each time the mouse scrolls. - _internalScrollbarPosition = std::clamp(newValue, 0.0, _core->BufferHeight()); + _internalScrollbarPosition = std::clamp(newValue, 0.0f, static_cast(_core->BufferHeight())); // If the new scrollbar position, rounded to an int, is at a different // row, then actually update the scroll position in the core, and raise // a ScrollPositionChanged to inform the control. - auto viewTop = ::base::saturated_cast(::std::round(_internalScrollbarPosition)); + const auto viewTop = std::lround(_internalScrollbarPosition); if (viewTop != _core->ScrollOffset()) { _core->UserScrollViewport(viewTop); diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index 81eeaca906..38789827ec 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -78,7 +78,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const Core::Point pixelPosition, const Control::MouseButtonState state); - void UpdateScrollbar(const double newValue); + void UpdateScrollbar(const float newValue); #pragma endregion @@ -110,8 +110,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine; winrt::com_ptr _core{ nullptr }; - unsigned int _rowsToScroll; - double _internalScrollbarPosition{ 0.0 }; + UINT _rowsToScroll = 3; + float _internalScrollbarPosition = 0; // If this is set, then we assume we are in the middle of panning the // viewport via touch input. diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl index 086b009ba0..d1ca9720dc 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.idl +++ b/src/cascadia/TerminalControl/ControlInteractivity.idl @@ -64,7 +64,7 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Core.Point pixelPosition, MouseButtonState state); - void UpdateScrollbar(Double newValue); + void UpdateScrollbar(Single newValue); Boolean ManglePathsForWsl { get; }; diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 247a8c988a..d8990c6f21 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -133,12 +133,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation struct TransparencyChangedEventArgs : public TransparencyChangedEventArgsT { public: - TransparencyChangedEventArgs(const double opacity) : + TransparencyChangedEventArgs(const float opacity) : _Opacity(opacity) { } - WINRT_PROPERTY(double, Opacity); + WINRT_PROPERTY(float, Opacity); }; struct UpdateSearchResultsEventArgs : public UpdateSearchResultsEventArgsT diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 19b31cd97d..d04044073e 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -75,7 +75,7 @@ namespace Microsoft.Terminal.Control runtimeclass TransparencyChangedEventArgs { - Double Opacity { get; }; + Single Opacity { get; }; } enum SearchState diff --git a/src/cascadia/TerminalControl/HwndTerminal.cpp b/src/cascadia/TerminalControl/HwndTerminal.cpp index 598028a88e..91cb3aaa4d 100644 --- a/src/cascadia/TerminalControl/HwndTerminal.cpp +++ b/src/cascadia/TerminalControl/HwndTerminal.cpp @@ -1090,9 +1090,9 @@ til::rect HwndTerminal::GetPadding() const noexcept return {}; } -double HwndTerminal::GetScaleFactor() const noexcept +float HwndTerminal::GetScaleFactor() const noexcept { - return static_cast(_currentDpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + return static_cast(_currentDpi) / static_cast(USER_DEFAULT_SCREEN_DPI); } void HwndTerminal::ChangeViewport(const til::inclusive_rect& NewWindow) diff --git a/src/cascadia/TerminalControl/HwndTerminal.hpp b/src/cascadia/TerminalControl/HwndTerminal.hpp index 45780a1d50..e561f1793f 100644 --- a/src/cascadia/TerminalControl/HwndTerminal.hpp +++ b/src/cascadia/TerminalControl/HwndTerminal.hpp @@ -146,7 +146,7 @@ private: // Inherited via IControlAccessibilityInfo til::size GetFontSize() const noexcept override; til::rect GetBounds() const noexcept override; - double GetScaleFactor() const noexcept override; + float GetScaleFactor() const noexcept override; void ChangeViewport(const til::inclusive_rect& NewWindow) override; HRESULT GetHostUiaProvider(IRawElementProviderSimple** provider) noexcept override; til::rect GetPadding() const noexcept override; diff --git a/src/cascadia/TerminalControl/IControlAppearance.idl b/src/cascadia/TerminalControl/IControlAppearance.idl index c94e637300..fca4be44d1 100644 --- a/src/cascadia/TerminalControl/IControlAppearance.idl +++ b/src/cascadia/TerminalControl/IControlAppearance.idl @@ -7,12 +7,12 @@ namespace Microsoft.Terminal.Control { Microsoft.Terminal.Core.Color SelectionBackground { get; }; String BackgroundImage { get; }; - Double BackgroundImageOpacity { get; }; + Single BackgroundImageOpacity { get; }; Windows.UI.Xaml.Media.Stretch BackgroundImageStretchMode { get; }; Windows.UI.Xaml.HorizontalAlignment BackgroundImageHorizontalAlignment { get; }; Windows.UI.Xaml.VerticalAlignment BackgroundImageVerticalAlignment { get; }; // IntenseIsBold and IntenseIsBright are in Core Appearance - Double Opacity { get; }; + Single Opacity { get; }; Boolean UseAcrylic { get; }; // Experimental settings diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp b/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp index 03f5006046..4640acac46 100644 --- a/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp @@ -169,14 +169,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _controlPadding; } - double InteractivityAutomationPeer::GetScaleFactor() const noexcept + float InteractivityAutomationPeer::GetScaleFactor() const noexcept { - return DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); + return static_cast(DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel()); } void InteractivityAutomationPeer::ChangeViewport(const til::inclusive_rect& NewWindow) { - _interactivity->UpdateScrollbar(NewWindow.top); + _interactivity->UpdateScrollbar(static_cast(NewWindow.top)); } #pragma endregion diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.h b/src/cascadia/TerminalControl/InteractivityAutomationPeer.h index fb3bdfe626..53240468f5 100644 --- a/src/cascadia/TerminalControl/InteractivityAutomationPeer.h +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.h @@ -66,7 +66,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation virtual til::size GetFontSize() const noexcept override; virtual til::rect GetBounds() const noexcept override; virtual til::rect GetPadding() const noexcept override; - virtual double GetScaleFactor() const noexcept override; + virtual float GetScaleFactor() const noexcept override; virtual void ChangeViewport(const til::inclusive_rect& NewWindow) override; virtual HRESULT GetHostUiaProvider(IRawElementProviderSimple** provider) override; #pragma endregion diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index acab829312..81934f10f6 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -779,10 +779,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation _interactivity.UpdateSettings(); if (_automationPeer) { - _automationPeer.SetControlPadding(Core::Padding{ newMargin.Left, - newMargin.Top, - newMargin.Right, - newMargin.Bottom }); + _automationPeer.SetControlPadding(Core::Padding{ + static_cast(newMargin.Left), + static_cast(newMargin.Top), + static_cast(newMargin.Right), + static_cast(newMargin.Bottom), + }); } _showMarksInScrollbar = settings.ShowMarks(); @@ -1050,10 +1052,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (const auto& interactivityAutoPeer{ _interactivity.OnCreateAutomationPeer() }) { const auto margins{ SwapChainPanel().Margin() }; - const Core::Padding padding{ margins.Left, - margins.Top, - margins.Right, - margins.Bottom }; + const Core::Padding padding{ + static_cast(margins.Left), + static_cast(margins.Top), + static_cast(margins.Right), + static_cast(margins.Bottom), + }; _automationPeer = winrt::make(get_strong(), padding, interactivityAutoPeer); return _automationPeer; } @@ -1254,10 +1258,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _automationPeer.UpdateControlBounds(); const auto margins{ GetPadding() }; - _automationPeer.SetControlPadding(Core::Padding{ margins.Left, - margins.Top, - margins.Right, - margins.Bottom }); + _automationPeer.SetControlPadding(Core::Padding{ + static_cast(margins.Left), + static_cast(margins.Top), + static_cast(margins.Right), + static_cast(margins.Bottom), + }); } // Likewise, run the event handlers outside of lock (they could @@ -1903,7 +1909,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto newValue = args.NewValue(); - _interactivity.UpdateScrollbar(newValue); + _interactivity.UpdateScrollbar(static_cast(newValue)); // User input takes priority over terminal events so cancel // any pending scroll bar update if the user scrolls. @@ -2451,14 +2457,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation // If the settings have negative or zero row or column counts, ignore those counts. // (The lower TerminalCore layer also has upper bounds as well, but at this layer // we may eventually impose different ones depending on how many pixels we can address.) - const auto cols = ::base::saturated_cast(std::max(commandlineCols > 0 ? - commandlineCols : - settings.InitialCols(), - 1)); - const auto rows = ::base::saturated_cast(std::max(commandlineRows > 0 ? - commandlineRows : - settings.InitialRows(), - 1)); + const auto cols = static_cast(std::max(commandlineCols > 0 ? + commandlineCols : + settings.InitialCols(), + 1)); + const auto rows = static_cast(std::max(commandlineRows > 0 ? + commandlineRows : + settings.InitialRows(), + 1)); const winrt::Windows::Foundation::Size initialSize{ cols, rows }; @@ -2763,14 +2769,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Convert text buffer cursor position to client coordinate position // within the window. This point is in _pixels_ - const double clientCursorPosX = terminalPos.x * fontSize.Width; - const double clientCursorPosY = terminalPos.y * fontSize.Height; + const auto clientCursorPosX = terminalPos.x * fontSize.Width; + const auto clientCursorPosY = terminalPos.y * fontSize.Height; // Get scale factor for view - const double scaleFactor = SwapChainPanel().CompositionScaleX(); + const auto scaleFactor = SwapChainPanel().CompositionScaleX(); - const double clientCursorInDipsX = clientCursorPosX / scaleFactor; - const double clientCursorInDipsY = clientCursorPosY / scaleFactor; + const auto clientCursorInDipsX = clientCursorPosX / scaleFactor; + const auto clientCursorInDipsY = clientCursorPosY / scaleFactor; auto padding{ GetPadding() }; til::point relativeToOrigin{ til::math::rounding, @@ -3504,7 +3510,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.ColorScheme(scheme); } - void TermControl::AdjustOpacity(const double opacity, const bool relative) + void TermControl::AdjustOpacity(const float opacity, const bool relative) { _core.AdjustOpacity(opacity, relative); } @@ -3513,7 +3519,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // defines an "Opacity", which we're actually not setting at all. We're // not overriding or changing _that_ value. Callers that want the opacity // set by the settings should call this instead. - double TermControl::BackgroundOpacity() const + float TermControl::BackgroundOpacity() const { return _core.Opacity(); } @@ -3646,7 +3652,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation cursorPos.y * fontSize.Height }; // Get scale factor for view - const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); + const auto scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); // Adjust to DIPs const til::point clientCursorInDips{ til::math::rounding, clientCursorPos.X / scaleFactor, clientCursorPos.Y / scaleFactor }; diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 4da7ec63d0..8568cb49b0 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -100,7 +100,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool BracketedPasteEnabled() const noexcept; - double BackgroundOpacity() const; + float BackgroundOpacity() const; uint64_t OwningHwnd(); void OwningHwnd(uint64_t owner); @@ -171,7 +171,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Microsoft::Terminal::Core::Scheme ColorScheme() const noexcept; void ColorScheme(const winrt::Microsoft::Terminal::Core::Scheme& scheme) const noexcept; - void AdjustOpacity(const double opacity, const bool relative); + void AdjustOpacity(const float opacity, const bool relative); bool RawWriteKeyEvent(const WORD vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown); bool RawWriteChar(const wchar_t character, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index d42afeffc2..ab17ab64d9 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -126,13 +126,13 @@ namespace Microsoft.Terminal.Control String ReadEntireBuffer(); CommandHistoryContext CommandHistory(); - void AdjustOpacity(Double Opacity, Boolean relative); + void AdjustOpacity(Single Opacity, Boolean relative); // You'd think this should just be "Opacity", but UIElement already // defines an "Opacity", which we're actually not setting at all. We're // not overriding or changing _that_ value. Callers that want the // opacity set by the settings should call this instead. - Double BackgroundOpacity { get; }; + Single BackgroundOpacity { get; }; CursorDisplayState CursorVisibility; diff --git a/src/cascadia/TerminalCore/ICoreAppearance.idl b/src/cascadia/TerminalCore/ICoreAppearance.idl index 6d314969e9..4ff3c60e5a 100644 --- a/src/cascadia/TerminalCore/ICoreAppearance.idl +++ b/src/cascadia/TerminalCore/ICoreAppearance.idl @@ -59,10 +59,10 @@ namespace Microsoft.Terminal.Core // 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; + Single Left; + Single Top; + Single Right; + Single Bottom; }; // This is a projection of Microsoft::Terminal::Core::ControlKeyStates, diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.cpp b/src/cascadia/TerminalSettingsEditor/Appearances.cpp index 15417166ad..036b045ca5 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.cpp +++ b/src/cascadia/TerminalSettingsEditor/Appearances.cpp @@ -537,7 +537,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void AppearanceViewModel::SetBackgroundImageOpacityFromPercentageValue(double percentageValue) { - BackgroundImageOpacity(winrt::Microsoft::Terminal::UI::Converters::PercentageValueToPercentage(percentageValue)); + BackgroundImageOpacity(static_cast(percentageValue) / 100.0f); } void AppearanceViewModel::SetBackgroundImagePath(winrt::hstring path) diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.idl b/src/cascadia/TerminalSettingsEditor/Appearances.idl index 88fb2c6f89..2855fa21b7 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.idl +++ b/src/cascadia/TerminalSettingsEditor/Appearances.idl @@ -91,7 +91,7 @@ namespace Microsoft.Terminal.Settings.Editor OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Microsoft.Terminal.Core.CursorStyle, CursorShape); OBSERVABLE_PROJECTED_APPEARANCE_SETTING(UInt32, CursorHeight); OBSERVABLE_PROJECTED_APPEARANCE_SETTING(String, BackgroundImagePath); - OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Double, BackgroundImageOpacity); + OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Single, BackgroundImageOpacity); OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Windows.UI.Xaml.Media.Stretch, BackgroundImageStretchMode); OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Microsoft.Terminal.Settings.Model.ConvergedAlignment, BackgroundImageAlignment); OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Microsoft.Terminal.Settings.Model.IntenseStyle, IntenseTextStyle); diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h index 3dd30223fe..04225a46ef 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -49,7 +49,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void SetAcrylicOpacityPercentageValue(double value) { - Opacity(winrt::Microsoft::Terminal::UI::Converters::PercentageValueToPercentage(value)); + Opacity(static_cast(value) / 100.0f); }; void SetPadding(double value) diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl index 135182a4fb..315afdfb4f 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -95,7 +95,7 @@ namespace Microsoft.Terminal.Settings.Editor OBSERVABLE_PROJECTED_PROFILE_SETTING(Windows.Foundation.IReference, TabColor); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, SuppressApplicationTitle); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, UseAcrylic); - OBSERVABLE_PROJECTED_PROFILE_SETTING(Double, Opacity); + OBSERVABLE_PROJECTED_PROFILE_SETTING(Single, Opacity); OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Control.ScrollbarState, ScrollState); OBSERVABLE_PROJECTED_PROFILE_SETTING(String, Padding); OBSERVABLE_PROJECTED_PROFILE_SETTING(String, Commandline); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index bf34a6af14..3f0a922b6b 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -630,12 +630,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation struct SplitPaneArgs : public SplitPaneArgsT { SplitPaneArgs() = default; - SplitPaneArgs(SplitType splitMode, SplitDirection direction, double size, const Model::INewContentArgs& terminalArgs) : + SplitPaneArgs(SplitType splitMode, SplitDirection direction, float size, const Model::INewContentArgs& terminalArgs) : _SplitMode{ splitMode }, _SplitDirection{ direction }, _SplitSize{ size }, _ContentArgs{ terminalArgs } {}; - SplitPaneArgs(SplitDirection direction, double size, const Model::INewContentArgs& terminalArgs) : + SplitPaneArgs(SplitDirection direction, float size, const Model::INewContentArgs& terminalArgs) : _SplitDirection{ direction }, _SplitSize{ size }, _ContentArgs{ terminalArgs } {}; @@ -648,7 +648,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARG(Model::SplitDirection, SplitDirection, SplitDirection::Automatic); WINRT_PROPERTY(Model::INewContentArgs, ContentArgs, nullptr); ACTION_ARG(SplitType, SplitMode, SplitType::Manual); - ACTION_ARG(double, SplitSize, .5); + ACTION_ARG(float, SplitSize, 0.5f); static constexpr std::string_view SplitKey{ "split" }; static constexpr std::string_view SplitModeKey{ "splitMode" }; diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 05ed70069a..af03e80717 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -230,15 +230,15 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass SplitPaneArgs : IActionArgs { - SplitPaneArgs(SplitType splitMode, SplitDirection split, Double size, INewContentArgs contentArgs); - SplitPaneArgs(SplitDirection split, Double size, INewContentArgs contentArgs); + SplitPaneArgs(SplitType splitMode, SplitDirection split, Single size, INewContentArgs contentArgs); + SplitPaneArgs(SplitDirection split, Single size, INewContentArgs contentArgs); SplitPaneArgs(SplitDirection split, INewContentArgs contentArgs); SplitPaneArgs(SplitType splitMode); SplitDirection SplitDirection { get; }; INewContentArgs ContentArgs { get; }; SplitType SplitMode { get; }; - Double SplitSize { get; }; + Single SplitSize { get; }; }; [default_interface] runtimeclass OpenSettingsArgs : IActionArgs diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp b/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp index 90513b146c..92af888c52 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp @@ -53,7 +53,7 @@ Json::Value AppearanceConfig::ToJson() const JsonUtils::SetValueForKey(json, BackgroundKey, _Background); JsonUtils::SetValueForKey(json, SelectionBackgroundKey, _SelectionBackground); JsonUtils::SetValueForKey(json, CursorColorKey, _CursorColor); - JsonUtils::SetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter{}); + JsonUtils::SetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter{}); if (HasDarkColorSchemeName() || HasLightColorSchemeName()) { // check if the setting is coming from the UI, if so grab the ColorSchemeName until the settings UI is fixed. @@ -95,7 +95,7 @@ void AppearanceConfig::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, CursorColorKey, _CursorColor); JsonUtils::GetValueForKey(json, LegacyAcrylicTransparencyKey, _Opacity); - JsonUtils::GetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter{}); + JsonUtils::GetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter{}); if (json["colorScheme"].isString()) { // to make the UI happy, set ColorSchemeName. diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h index 7e8f35e663..cffbdede6d 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h @@ -40,7 +40,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Background, nullptr); INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, SelectionBackground, nullptr); INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, CursorColor, nullptr); - INHERITABLE_SETTING(Model::IAppearanceConfig, double, Opacity, 1.0); + INHERITABLE_SETTING(Model::IAppearanceConfig, float, Opacity, 1.0f); INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, DarkColorSchemeName, L"Campbell"); INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, LightColorSchemeName, L"Campbell"); diff --git a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl index bf8d8c5ed9..f05cb85399 100644 --- a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl +++ b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl @@ -45,7 +45,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_APPEARANCE_SETTING(String, BackgroundImagePath); String ExpandedBackgroundImagePath { get; }; - INHERITABLE_APPEARANCE_SETTING(Double, BackgroundImageOpacity); + INHERITABLE_APPEARANCE_SETTING(Single, BackgroundImageOpacity); INHERITABLE_APPEARANCE_SETTING(Windows.UI.Xaml.Media.Stretch, BackgroundImageStretchMode); INHERITABLE_APPEARANCE_SETTING(ConvergedAlignment, BackgroundImageAlignment); @@ -54,7 +54,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_APPEARANCE_SETTING(String, PixelShaderImagePath); INHERITABLE_APPEARANCE_SETTING(IntenseStyle, IntenseTextStyle); INHERITABLE_APPEARANCE_SETTING(Microsoft.Terminal.Core.AdjustTextMode, AdjustIndistinguishableColors); - INHERITABLE_APPEARANCE_SETTING(Double, Opacity); + INHERITABLE_APPEARANCE_SETTING(Single, Opacity); INHERITABLE_APPEARANCE_SETTING(Boolean, UseAcrylic); }; } diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index 6b1804d519..ec94d23c92 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -729,7 +729,7 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils return json.isNumeric(); } - Json::Value ToJson(const float& val) + Json::Value ToJson(const float val) { return val; } @@ -753,7 +753,7 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils return json.isNumeric(); } - Json::Value ToJson(const double& val) + Json::Value ToJson(const double val) { return val; } diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 0375d2802f..f0e30684c5 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -121,7 +121,7 @@ Author(s): #define MTSM_APPEARANCE_SETTINGS(X) \ X(Core::CursorStyle, CursorShape, "cursorShape", Core::CursorStyle::Bar) \ X(uint32_t, CursorHeight, "cursorHeight", DEFAULT_CURSOR_HEIGHT) \ - X(double, BackgroundImageOpacity, "backgroundImageOpacity", 1.0) \ + X(float, BackgroundImageOpacity, "backgroundImageOpacity", 1.0f) \ X(winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, "backgroundImageStretchMode", winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill) \ X(bool, RetroTerminalEffect, "experimental.retroTerminalEffect", false) \ X(hstring, PixelShaderPath, "experimental.pixelShaderPath") \ diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 57ec8659fd..45b4db2aa4 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -121,7 +121,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, guid, SessionId); INHERITABLE_SETTING(Model::TerminalSettings, bool, EnableUnfocusedAcrylic, false); INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAcrylic, false); - INHERITABLE_SETTING(Model::TerminalSettings, double, Opacity, UseAcrylic() ? 0.5 : 1.0); + INHERITABLE_SETTING(Model::TerminalSettings, float, Opacity, UseAcrylic() ? 0.5f : 1.0f); INHERITABLE_SETTING(Model::TerminalSettings, hstring, Padding, DEFAULT_PADDING); INHERITABLE_SETTING(Model::TerminalSettings, hstring, FontFace, DEFAULT_FONT_FACE); INHERITABLE_SETTING(Model::TerminalSettings, float, FontSize, DEFAULT_FONT_SIZE); @@ -136,7 +136,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, Model::ColorScheme, AppliedColorScheme); INHERITABLE_SETTING(Model::TerminalSettings, hstring, BackgroundImage); - INHERITABLE_SETTING(Model::TerminalSettings, double, BackgroundImageOpacity, 1.0); + INHERITABLE_SETTING(Model::TerminalSettings, float, BackgroundImageOpacity, 1.0f); INHERITABLE_SETTING(Model::TerminalSettings, winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill); INHERITABLE_SETTING(Model::TerminalSettings, winrt::Windows::UI::Xaml::HorizontalAlignment, BackgroundImageHorizontalAlignment, winrt::Windows::UI::Xaml::HorizontalAlignment::Center); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 1168e0660f..d15c65c0fa 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -353,11 +353,11 @@ struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<::winr } }; -struct IntAsFloatPercentConversionTrait : ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait +struct IntAsFloatPercentConversionTrait : ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait { - double FromJson(const Json::Value& json) + float FromJson(const Json::Value& json) { - return ::base::saturated_cast(json.asUInt()) / 100.0; + return static_cast(json.asUInt()) / 100.0f; } bool CanConvert(const Json::Value& json) @@ -370,9 +370,9 @@ struct IntAsFloatPercentConversionTrait : ::Microsoft::Terminal::Settings::Model return value >= 0 && value <= 100; } - Json::Value ToJson(const double& val) + Json::Value ToJson(const float val) { - return std::clamp(::base::saturated_cast(std::round(val * 100.0)), 0u, 100u); + return std::clamp(::base::saturated_cast(std::round(val * 100.0f)), 0u, 100u); } std::string TypeDescription() const diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index f4657e8252..aeefdca995 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -137,14 +137,14 @@ namespace ControlUnitTests VERIFY_IS_NOT_NULL(core); // A callback to make sure that we're raising TransparencyChanged events - auto expectedOpacity = 0.5; + auto expectedOpacity = 0.5f; auto opacityCallback = [&](auto&&, Control::TransparencyChangedEventArgs args) mutable { VERIFY_ARE_EQUAL(expectedOpacity, args.Opacity()); VERIFY_ARE_EQUAL(expectedOpacity, core->Opacity()); // The Settings object's opacity shouldn't be changed - VERIFY_ARE_EQUAL(0.5, settings->Opacity()); + VERIFY_ARE_EQUAL(0.5f, settings->Opacity()); - if (expectedOpacity < 1.0) + if (expectedOpacity < 1.0f) { VERIFY_IS_TRUE(settings->UseAcrylic()); VERIFY_IS_TRUE(core->_settings->UseAcrylic()); @@ -153,7 +153,7 @@ namespace ControlUnitTests // GH#603: Adjusting opacity shouldn't change whether or not we // requested acrylic. - auto expectedUseAcrylic = expectedOpacity < 1.0; + auto expectedUseAcrylic = expectedOpacity < 1.0f; VERIFY_IS_TRUE(core->_settings->UseAcrylic()); VERIFY_ARE_EQUAL(expectedUseAcrylic, core->UseAcrylic()); }; @@ -166,39 +166,39 @@ namespace ControlUnitTests VERIFY_IS_TRUE(core->_initializedTerminal); Log::Comment(L"Increasing opacity till fully opaque"); - expectedOpacity += 0.1; // = 0.6; - core->AdjustOpacity(0.1); - expectedOpacity += 0.1; // = 0.7; - core->AdjustOpacity(0.1); - expectedOpacity += 0.1; // = 0.8; - core->AdjustOpacity(0.1); - expectedOpacity += 0.1; // = 0.9; - core->AdjustOpacity(0.1); - expectedOpacity += 0.1; // = 1.0; + expectedOpacity += 0.1f; // = 0.6; + core->AdjustOpacity(0.1f); + expectedOpacity += 0.1f; // = 0.7; + core->AdjustOpacity(0.1f); + expectedOpacity += 0.1f; // = 0.8; + core->AdjustOpacity(0.1f); + expectedOpacity += 0.1f; // = 0.9; + core->AdjustOpacity(0.1f); + expectedOpacity += 0.1f; // = 1.0; // cast to float because floating point numbers are mean - VERIFY_ARE_EQUAL(1.0f, base::saturated_cast(expectedOpacity)); - core->AdjustOpacity(0.1); + VERIFY_ARE_EQUAL(1.0f, expectedOpacity); + core->AdjustOpacity(0.1f); Log::Comment(L"Increasing opacity more doesn't actually change it to be >1.0"); - expectedOpacity = 1.0; - core->AdjustOpacity(0.1); + expectedOpacity = 1.0f; + core->AdjustOpacity(0.1f); Log::Comment(L"Decrease opacity"); - expectedOpacity -= 0.25; // = 0.75; - core->AdjustOpacity(-0.25); - expectedOpacity -= 0.25; // = 0.5; - core->AdjustOpacity(-0.25); - expectedOpacity -= 0.25; // = 0.25; - core->AdjustOpacity(-0.25); - expectedOpacity -= 0.25; // = 0.05; + expectedOpacity -= 0.25f; // = 0.75; + core->AdjustOpacity(-0.25f); + expectedOpacity -= 0.25f; // = 0.5; + core->AdjustOpacity(-0.25f); + expectedOpacity -= 0.25f; // = 0.25; + core->AdjustOpacity(-0.25f); + expectedOpacity -= 0.25f; // = 0.05; // cast to float because floating point numbers are mean - VERIFY_ARE_EQUAL(0.0f, base::saturated_cast(expectedOpacity)); - core->AdjustOpacity(-0.25); + VERIFY_ARE_EQUAL(0.0f, expectedOpacity); + core->AdjustOpacity(-0.25f); Log::Comment(L"Decreasing opacity more doesn't actually change it to be < 0"); - expectedOpacity = 0.0; - core->AdjustOpacity(-0.25); + expectedOpacity = 0.0f; + core->AdjustOpacity(-0.25f); } void ControlCoreTests::TestFreeAfterClose() diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index 192cc2ad9d..f95d9f0f16 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -140,14 +140,14 @@ namespace ControlUnitTests auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); // A callback to make sure that we're raising TransparencyChanged events - auto expectedOpacity = 0.5; + auto expectedOpacity = 0.5f; auto opacityCallback = [&](auto&&, Control::TransparencyChangedEventArgs args) mutable { VERIFY_ARE_EQUAL(expectedOpacity, args.Opacity()); VERIFY_ARE_EQUAL(expectedOpacity, core->Opacity()); // The Settings object's opacity shouldn't be changed - VERIFY_ARE_EQUAL(0.5, settings->Opacity()); + VERIFY_ARE_EQUAL(0.5f, settings->Opacity()); - auto expectedUseAcrylic = expectedOpacity < 1.0 && + auto expectedUseAcrylic = expectedOpacity < 1.0f && (useAcrylic); VERIFY_ARE_EQUAL(useAcrylic, settings->UseAcrylic()); VERIFY_ARE_EQUAL(expectedUseAcrylic, core->UseAcrylic()); @@ -162,10 +162,10 @@ namespace ControlUnitTests for (auto i = 0; i < 55; i++) { // each mouse wheel only adjusts opacity by .01 - expectedOpacity += 0.01; - if (expectedOpacity >= 1.0) + expectedOpacity += 0.01f; + if (expectedOpacity >= 1.0f) { - expectedOpacity = 1.0; + expectedOpacity = 1.0f; } // The mouse location and buttons don't matter here. @@ -180,10 +180,10 @@ namespace ControlUnitTests for (auto i = 0; i < 105; i++) { // each mouse wheel only adjusts opacity by .01 - expectedOpacity -= 0.01; - if (expectedOpacity <= 0.0) + expectedOpacity -= 0.01f; + if (expectedOpacity <= 0.0f) { - expectedOpacity = 0.0; + expectedOpacity = 0.0f; } // The mouse location and buttons don't matter here. diff --git a/src/cascadia/UnitTests_SettingsModel/CommandTests.cpp b/src/cascadia/UnitTests_SettingsModel/CommandTests.cpp index b4940852c9..4ef9c8a153 100644 --- a/src/cascadia/UnitTests_SettingsModel/CommandTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/CommandTests.cpp @@ -156,7 +156,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } { auto command = commands.Lookup(L"command2"); @@ -167,7 +167,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Down, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } { auto command = commands.Lookup(L"command4"); @@ -178,7 +178,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Automatic, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } { auto command = commands.Lookup(L"command5"); @@ -189,7 +189,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Automatic, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } { auto command = commands.Lookup(L"command6"); @@ -211,7 +211,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } { auto command = commands.Lookup(L"command8"); @@ -222,7 +222,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Left, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } { auto command = commands.Lookup(L"command9"); @@ -233,7 +233,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Up, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } { auto command = commands.Lookup(L"command10"); @@ -244,7 +244,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Down, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.5, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.5f, realArgs.SplitSize()); } } @@ -274,7 +274,7 @@ namespace SettingsModelUnitTests VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Automatic, realArgs.SplitDirection()); - VERIFY_ARE_EQUAL(0.25, realArgs.SplitSize()); + VERIFY_ARE_EQUAL(0.25f, realArgs.SplitSize()); } } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index ab800163ff..44e8fb3745 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -1423,7 +1423,7 @@ void IslandWindow::_doSlideAnimation(const uint32_t dropdownDuration, const bool { const auto end = std::chrono::system_clock::now(); const auto millis = std::chrono::duration_cast(end - start); - const auto dt = ::base::saturated_cast(millis.count()); + const auto dt = static_cast(millis.count()); if (dt > animationDuration) { diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index 0c4a50308d..f848f401b6 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -19,10 +19,10 @@ // All of these settings are defined in IControlAppearance. #define CONTROL_APPEARANCE_SETTINGS(X) \ X(til::color, SelectionBackground, DEFAULT_FOREGROUND) \ - X(double, Opacity, 1.0) \ + X(float, Opacity, 1.0f) \ X(bool, UseAcrylic, false) \ X(winrt::hstring, BackgroundImage) \ - X(double, BackgroundImageOpacity, 1.0) \ + X(float, BackgroundImageOpacity, 1.0f) \ X(winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill) \ X(winrt::Windows::UI::Xaml::HorizontalAlignment, BackgroundImageHorizontalAlignment, winrt::Windows::UI::Xaml::HorizontalAlignment::Center) \ X(winrt::Windows::UI::Xaml::VerticalAlignment, BackgroundImageVerticalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment::Center) \ diff --git a/src/types/IControlAccessibilityInfo.h b/src/types/IControlAccessibilityInfo.h index 128feff998..0bbe9fcf80 100644 --- a/src/types/IControlAccessibilityInfo.h +++ b/src/types/IControlAccessibilityInfo.h @@ -27,7 +27,7 @@ namespace Microsoft::Console::Types virtual til::size GetFontSize() const noexcept = 0; virtual til::rect GetBounds() const noexcept = 0; virtual til::rect GetPadding() const noexcept = 0; - virtual double GetScaleFactor() const noexcept = 0; + virtual float GetScaleFactor() const noexcept = 0; virtual void ChangeViewport(const til::inclusive_rect& NewWindow) = 0; virtual HRESULT GetHostUiaProvider(IRawElementProviderSimple** provider) = 0; diff --git a/src/types/TermControlUiaProvider.cpp b/src/types/TermControlUiaProvider.cpp index b41b819139..3cfc041f5d 100644 --- a/src/types/TermControlUiaProvider.cpp +++ b/src/types/TermControlUiaProvider.cpp @@ -145,7 +145,7 @@ til::rect TermControlUiaProvider::GetPadding() const noexcept return _controlInfo->GetPadding(); } -double TermControlUiaProvider::GetScaleFactor() const noexcept +float TermControlUiaProvider::GetScaleFactor() const noexcept { return _controlInfo->GetScaleFactor(); } diff --git a/src/types/TermControlUiaProvider.hpp b/src/types/TermControlUiaProvider.hpp index 6cc8a7774c..ff2e244141 100644 --- a/src/types/TermControlUiaProvider.hpp +++ b/src/types/TermControlUiaProvider.hpp @@ -45,7 +45,7 @@ namespace Microsoft::Terminal til::size GetFontSize() const noexcept; til::rect GetPadding() const noexcept; - double GetScaleFactor() const noexcept; + float GetScaleFactor() const noexcept; void ChangeViewport(const til::inclusive_rect& NewWindow) override; protected: From 19c24aced98057ca40cfc8356c4bd3060e1eca7e Mon Sep 17 00:00:00 2001 From: Jvr <109031036+Jvr2022@users.noreply.github.com> Date: Tue, 23 Apr 2024 18:16:46 +0200 Subject: [PATCH 10/53] Update actions/add-to-project to 1.0.1 (#17097) --- .github/workflows/addToProject.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/addToProject.yml b/.github/workflows/addToProject.yml index 4baa537dd1..e756dec59d 100644 --- a/.github/workflows/addToProject.yml +++ b/.github/workflows/addToProject.yml @@ -13,7 +13,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.5.0 + - uses: actions/add-to-project@v1.0.1 with: project-url: https://github.com/orgs/microsoft/projects/159 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} From a590a1bff076317a1f0d1fd7565af4b6f0f0acf3 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 23 Apr 2024 18:18:12 +0200 Subject: [PATCH 11/53] Fix TerminalPage not being released on window close (#17107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because this holds onto the root element, `TerminalPage` gets "leaked" on Windows 10 when a window is closed until another is opened. ## Validation Steps Performed * Set a breakpoint in `Renderer::~Renderer` * Open and close a window * Breakpoint used to not get hit and now it does ✅ --- src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp | 2 -- src/cascadia/WindowsTerminal/NonClientIslandWindow.h | 1 - 2 files changed, 3 deletions(-) diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index bab6513226..73a2ea8944 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -405,8 +405,6 @@ bool NonClientIslandWindow::Initialize() // - void NonClientIslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) { - _clientContent = content; - _rootGrid.Children().Append(content); // SetRow only works on FrameworkElement's, so cast it to a FWE before diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index b2cbb5a8a2..bf9c815237 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -58,7 +58,6 @@ private: std::optional _oldIslandPos; winrt::TerminalApp::TitlebarControl _titlebar{ nullptr }; - winrt::Windows::UI::Xaml::UIElement _clientContent{ nullptr }; wil::unique_hbrush _backgroundBrush; til::color _backgroundBrushColor; From daffb2dbbffaeb78a5d9763f0c222927cf3af109 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 23 Apr 2024 18:19:38 +0200 Subject: [PATCH 12/53] AtlasEngine: Fix custom shader time imprecision (#17104) Since floats are imprecise we need to constrain the time value into a range that can be accurately represented. Assuming a monitor refresh rate of 1000 Hz, we can still easily represent 1000 seconds accurately (roughly 16 minutes). So to solve this, we'll simply treat the shader time modulo 1000s. This may lead to some unexpected jank every 16min but it keeps any ongoing animation smooth otherwise. --- src/renderer/atlas/BackendD3D.cpp | 29 +++++++++++++++++++++++++++-- src/renderer/atlas/BackendD3D.h | 3 ++- src/renderer/atlas/common.h | 2 ++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 6d7a86c5e4..d6fb766f33 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -47,6 +47,20 @@ using namespace Microsoft::Console::Render::Atlas; static constexpr D2D1_MATRIX_3X2_F identityTransform{ .m11 = 1, .m22 = 1 }; static constexpr D2D1_COLOR_F whiteColor{ 1, 1, 1, 1 }; +static u64 queryPerfFreq() noexcept +{ + LARGE_INTEGER li; + QueryPerformanceFrequency(&li); + return std::bit_cast(li.QuadPart); +} + +static u64 queryPerfCount() noexcept +{ + LARGE_INTEGER li; + QueryPerformanceCounter(&li); + return std::bit_cast(li.QuadPart); +} + BackendD3D::BackendD3D(const RenderingPayload& p) { THROW_IF_FAILED(p.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _vertexShader.addressof())); @@ -485,7 +499,14 @@ void BackendD3D::_recreateCustomShader(const RenderingPayload& p) THROW_IF_FAILED(p.device->CreateSamplerState(&desc, _customShaderSamplerState.put())); } - _customShaderStartTime = std::chrono::steady_clock::now(); + // Since floats are imprecise we need to constrain the time value into a range that can be accurately represented. + // Assuming a monitor refresh rate of 1000 Hz, we can still easily represent 1000 seconds accurately (roughly 16 minutes). + // 10000 seconds would already result in a 50% error. So to avoid this, we use queryPerfCount() modulo _customShaderPerfTickMod. + // The use of a power of 10 is intentional, because shaders are often periodic and this makes any decimal multiplier up to 3 fractional + // digits not break the periodicity. For instance, with a wraparound of 1000 seconds sin(1.234*x) is still perfectly periodic. + const auto freq = queryPerfFreq(); + _customShaderPerfTickMod = freq * 1000; + _customShaderSecsPerPerfTick = 1.0f / freq; } } @@ -2111,8 +2132,12 @@ void BackendD3D::_debugDumpRenderTarget(const RenderingPayload& p) void BackendD3D::_executeCustomShader(RenderingPayload& p) { { + // See the comment in _recreateCustomShader() which initializes the two members below and explains what they do. + const auto now = queryPerfCount(); + const auto time = static_cast(now % _customShaderPerfTickMod) * _customShaderSecsPerPerfTick; + const CustomConstBuffer data{ - .time = std::chrono::duration(std::chrono::steady_clock::now() - _customShaderStartTime).count(), + .time = time, .scale = static_cast(p.s->font->dpi) / static_cast(USER_DEFAULT_SCREEN_DPI), .resolution = { static_cast(_viewportCellCount.x * p.s->font->cellSize.x), diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index 6a12e704a4..ae9803d51f 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -248,7 +248,8 @@ namespace Microsoft::Console::Render::Atlas wil::com_ptr _customShaderSamplerState; wil::com_ptr _customShaderTexture; wil::com_ptr _customShaderTextureView; - std::chrono::steady_clock::time_point _customShaderStartTime; + u64 _customShaderPerfTickMod = 0; + f32 _customShaderSecsPerPerfTick = 0; wil::com_ptr _backgroundBitmap; wil::com_ptr _backgroundBitmapView; diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h index ad9bf2587b..120b3d8666 100644 --- a/src/renderer/atlas/common.h +++ b/src/renderer/atlas/common.h @@ -146,6 +146,8 @@ namespace Microsoft::Console::Render::Atlas using i32x4 = vec4; using i32r = rect; + using u64 = uint64_t; + using f32 = float; using f32x2 = vec2; using f32x4 = vec4; From 87a9f72b9a323ef2e49db9e74bd740cc9c2aab31 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 23 Apr 2024 12:41:59 -0700 Subject: [PATCH 13/53] Don't explode when duplicating a pane (#17110) I forgot to check here if the `INewContentArgs` were null or not. Pretty dumb mistake honestly. Closes #17075 Closes #17076 --- src/cascadia/TerminalApp/TerminalPage.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index b10e2157c1..3a82a05aeb 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -3152,7 +3152,8 @@ namespace winrt::TerminalApp::implementation TerminalConnection::ITerminalConnection existingConnection) { - if (const auto& newTerminalArgs{ contentArgs.try_as() }) + const auto& newTerminalArgs{ contentArgs.try_as() }; + if (contentArgs == nullptr || newTerminalArgs != nullptr || contentArgs.Type().empty()) { // Terminals are of course special, and have to deal with debug taps, duplicating the tab, etc. return _MakeTerminalPane(newTerminalArgs, sourceTab, existingConnection); From 360e86b5368f229c8ece4a332ebf62b3b2bdb215 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 24 Apr 2024 00:04:35 +0200 Subject: [PATCH 14/53] Fix search highlights during reflow (#17092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR extends `til::throttled_func` to also support debouncing: * throttling: "At most 1 call every N seconds" * debouncing: "Exactly 1 call after N seconds of inactivity" Based on the latter the following series of changes were made: * An `OutputIdle` event was added to `ControlCore` which is raised once there hasn't been any incoming data in 100ms. This also triggers an update of our regex patterns (URL detection). * The event is then caught by `TermControl` which calls `Search()`. * `Search()` in turn was modified to return its results by-value as a struct, which avoids the need for a search-update event and simplifies how we update the UI. This architectural change, most importantly the removal of the `TextLayoutUpdated` event, fixes a DoS bug in Windows Terminal: As the event leads to UI thread activity, printing lots of text continuously results in the UI thread becoming unresponsive. On top of these, a number of improvements were made: * `IRenderEngine::InvalidateHighlight` was changed to take the `TextBuffer` by-reference which avoids the need to accumulate the line renditions in a `std::vector` first. This improves Debug build performance during reflow by what I guess must be roughly a magnitude faster. This difference is very noticeable. * When closing the search box, `ClearSearch()` is called to remove the highlights. The search text is restored when it's reopened, however the current search position isn't. Closes #17073 Closes #17089 ## Validation Steps Performed * UIA announcements: * Pressing Ctrl+Shift+F the first time does not lead to one ✅ * Typing the first letter does ✅ * Closing doesn't ✅ * Reopening does (as it restores the letter) ✅ * Closing the search box dismisses the highlights ✅ * Resizing the window recalculates the highlights ✅ * Changing the terminal output while the box is open recalculates the highlights ✅ --- src/buffer/out/search.cpp | 14 +- src/cascadia/TerminalControl/ControlCore.cpp | 135 ++++++++---------- src/cascadia/TerminalControl/ControlCore.h | 8 +- src/cascadia/TerminalControl/ControlCore.idl | 12 +- src/cascadia/TerminalControl/EventArgs.cpp | 1 - src/cascadia/TerminalControl/EventArgs.h | 12 -- src/cascadia/TerminalControl/EventArgs.idl | 8 -- .../TerminalControl/SearchBoxControl.cpp | 37 ++--- .../TerminalControl/SearchBoxControl.h | 7 +- .../TerminalControl/SearchBoxControl.idl | 2 - src/cascadia/TerminalControl/TermControl.cpp | 108 +++++++------- src/cascadia/TerminalControl/TermControl.h | 8 +- src/cascadia/TerminalCore/Terminal.cpp | 5 - src/cascadia/TerminalCore/Terminal.hpp | 3 - src/cascadia/TerminalCore/TerminalApi.cpp | 16 --- src/host/outputStream.cpp | 7 +- src/host/outputStream.hpp | 1 - src/inc/til/throttled_func.h | 54 ++++--- src/renderer/atlas/AtlasEngine.api.cpp | 5 +- src/renderer/atlas/AtlasEngine.h | 2 +- src/renderer/base/RenderEngineBase.cpp | 2 +- src/renderer/base/renderer.cpp | 21 ++- src/renderer/inc/IRenderEngine.hpp | 2 +- src/renderer/inc/RenderEngineBase.hpp | 2 +- src/terminal/adapter/ITerminalApi.hpp | 1 - src/terminal/adapter/adaptDispatch.cpp | 1 - .../adapter/ut_adapter/adapterTest.cpp | 5 - 27 files changed, 203 insertions(+), 276 deletions(-) diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index 336fa2957f..08ed8ad2ef 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -13,7 +13,8 @@ bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, c const auto& textBuffer = renderData.GetTextBuffer(); const auto lastMutationId = textBuffer.GetLastMutationId(); - if (_needle == needle && + if (_renderData == &renderData && + _needle == needle && _caseInsensitive == caseInsensitive && _lastMutationId == lastMutationId) { @@ -21,15 +22,16 @@ bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, c return false; } + if (prevResults) + { + *prevResults = std::move(_results); + } + _renderData = &renderData; _needle = needle; _caseInsensitive = caseInsensitive; _lastMutationId = lastMutationId; - if (prevResults) - { - *prevResults = std::move(_results); - } _results = textBuffer.SearchText(needle, caseInsensitive); _index = reverse ? gsl::narrow_cast(_results.size()) - 1 : 0; _step = reverse ? -1 : 1; @@ -142,4 +144,4 @@ const std::vector& Search::Results() const noexcept ptrdiff_t Search::CurrentMatch() const noexcept { return _index; -} \ No newline at end of file +} diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index cfba0a1d46..1e82b825eb 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -29,19 +29,6 @@ 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); - -// The delay before performing the search after change of search criteria -constexpr const auto SearchAfterChangeDelay = std::chrono::milliseconds(200); - namespace winrt::Microsoft::Terminal::Control::implementation { static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const til::color& c) noexcept @@ -117,9 +104,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnShowWindowChanged = std::bind(&ControlCore::_terminalShowWindowChanged, this, std::placeholders::_1); _terminal->SetShowWindowCallback(pfnShowWindowChanged); - auto pfnTextLayoutUpdated = std::bind(&ControlCore::_terminalTextLayoutUpdated, this); - _terminal->SetTextLayoutUpdatedCallback(pfnTextLayoutUpdated); - auto pfnPlayMidiNote = std::bind(&ControlCore::_terminalPlayMidiNote, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); _terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote); @@ -167,21 +151,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation _dispatcher = controller.DispatcherQueue(); } - // A few different events should be throttled, so they don't fire absolutely all the time: - // * _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. const auto shared = _shared.lock(); + // Raises an OutputIdle event once there hasn't been any output for at least 100ms. + // It also updates all regex patterns in the viewport. + // // NOTE: Calling UpdatePatternLocations from a background // thread is a workaround for us to hit GH#12607 less often. - shared->updatePatternLocations = std::make_unique>( - UpdatePatternLocationsInterval, - [weakTerminal = std::weak_ptr{ _terminal }]() { + shared->outputIdle = std::make_unique>( + std::chrono::milliseconds{ 100 }, + [weakTerminal = std::weak_ptr{ _terminal }, weakThis = get_weak(), dispatcher = _dispatcher]() { + dispatcher.TryEnqueue(DispatcherQueuePriority::Normal, [weakThis]() { + if (const auto self = weakThis.get(); !self->_IsClosing()) + { + self->OutputIdle.raise(*self, nullptr); + } + }); + if (const auto t = weakTerminal.lock()) { const auto lock = t->LockForWriting(); @@ -189,9 +174,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation } }); + // Scrollbar updates are also expensive (XAML), so we'll throttle them as well. shared->updateScrollBar = std::make_shared>( _dispatcher, - ScrollBarUpdateInterval, + std::chrono::milliseconds{ 8 }, [weakThis = get_weak()](const auto& update) { if (auto core{ weakThis.get() }; !core->_IsClosing()) { @@ -218,7 +204,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // thread. These will be recreated in _setupDispatcherAndCallbacks, when // we're re-attached to a new control (on a possibly new UI thread). const auto shared = _shared.lock(); - shared->updatePatternLocations.reset(); + shared->outputIdle.reset(); shared->updateScrollBar.reset(); } @@ -671,9 +657,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto shared = _shared.lock_shared(); - if (shared->updatePatternLocations) + if (shared->outputIdle) { - (*shared->updatePatternLocations)(); + (*shared->outputIdle)(); } } @@ -1100,12 +1086,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation // 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 auto hr = _terminal->UserResize({ vp.Width(), vp.Height() }); - if (SUCCEEDED(hr) && hr != S_FALSE) + if (FAILED(hr) || hr == S_FALSE) { - _connection.Resize(vp.Height(), vp.Width()); + return; + } - // let the UI know that the text layout has been updated - _terminal->NotifyTextLayoutUpdated(); + _connection.Resize(vp.Height(), vp.Width()); + + // TermControl will call Search() once the OutputIdle even fires after 100ms. + // Until then we need to hide the now-stale search results from the renderer. + ClearSearch(); + const auto shared = _shared.lock_shared(); + if (shared->outputIdle) + { + (*shared->outputIdle)(); } } @@ -1603,16 +1597,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation ShowWindowChanged.raise(*this, *showWindow); } - void ControlCore::_terminalTextLayoutUpdated() - { - ClearSearch(); - - // send an UpdateSearchResults event to the UI to put the Search UI into inactive state. - auto evArgs = winrt::make_self(); - evArgs->State(SearchState::Inactive); - UpdateSearchResults.raise(*this, *evArgs); - } - // Method Description: // - Plays a single MIDI note, blocking for the duration. // Arguments: @@ -1672,13 +1656,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - caseSensitive: boolean that represents if the current search is case sensitive // Return Value: // - - void ControlCore::Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive) + SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool reset) { const auto lock = _terminal->LockForWriting(); + bool searchInvalidated = false; std::vector oldResults; if (_searcher.ResetIfStale(*GetRenderData(), text, !goForward, !caseSensitive, &oldResults)) { + searchInvalidated = true; + _cachedSearchResultRows = {}; if (SnapSearchResultToSelection()) { @@ -1687,30 +1674,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation } _terminal->SetSearchHighlights(_searcher.Results()); - _terminal->SetSearchHighlightFocused(_searcher.CurrentMatch()); } - else + else if (!reset) { _searcher.FindNext(); - _terminal->SetSearchHighlightFocused(_searcher.CurrentMatch()); } + + int32_t totalMatches = 0; + int32_t currentMatch = 0; + if (const auto idx = _searcher.CurrentMatch(); idx >= 0) + { + totalMatches = gsl::narrow(_searcher.Results().size()); + currentMatch = gsl::narrow(idx); + _terminal->SetSearchHighlightFocused(gsl::narrow(idx)); + } + _renderer->TriggerSearchHighlight(oldResults); - auto evArgs = winrt::make_self(); - if (!text.empty()) - { - evArgs->State(SearchState::Active); - if (_searcher.GetCurrent()) - { - evArgs->FoundMatch(true); - evArgs->TotalMatches(gsl::narrow(_searcher.Results().size())); - evArgs->CurrentMatch(gsl::narrow(_searcher.CurrentMatch())); - } - } - - // Raise an UpdateSearchResults event, which the control will use to update the - // UI and notify the narrator about the updated search results in the buffer - UpdateSearchResults.raise(*this, *evArgs); + return { + .TotalMatches = totalMatches, + .CurrentMatch = currentMatch, + .SearchInvalidated = searchInvalidated, + }; } Windows::Foundation::Collections::IVector ControlCore::SearchResultRows() @@ -1738,16 +1723,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::ClearSearch() { - // nothing to clear if there's no results - if (_searcher.GetCurrent()) - { - const auto lock = _terminal->LockForWriting(); - _terminal->SetSearchHighlights({}); - _terminal->SetSearchHighlightFocused({}); - _renderer->TriggerSearchHighlight(_searcher.Results()); - _searcher = {}; - _cachedSearchResultRows = {}; - } + const auto lock = _terminal->LockForWriting(); + _terminal->SetSearchHighlights({}); + _terminal->SetSearchHighlightFocused({}); + _renderer->TriggerSearchHighlight(_searcher.Results()); + _searcher = {}; + _cachedSearchResultRows = {}; } // Method Description: @@ -2130,9 +2111,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Start the throttled update of where our hyperlinks are. const auto shared = _shared.lock_shared(); - if (shared->updatePatternLocations) + if (shared->outputIdle) { - (*shared->updatePatternLocations)(); + (*shared->outputIdle)(); } } catch (...) diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index ee2e1e5a84..adcaf82102 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -219,7 +219,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetSelectionAnchor(const til::point position); void SetEndSelectionPoint(const til::point position); - void Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); + SearchResults Search(const std::wstring_view& text, bool goForward, bool caseSensitive, bool reset); void ClearSearch(); void SnapSearchResultToSelection(bool snap) noexcept; bool SnapSearchResultToSelection() const noexcept; @@ -279,8 +279,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event RendererWarning; til::typed_event RaiseNotice; til::typed_event TransparencyChanged; - til::typed_event<> ReceivedOutput; - til::typed_event UpdateSearchResults; + til::typed_event<> OutputIdle; til::typed_event ShowWindowChanged; til::typed_event UpdateSelectionMarkers; til::typed_event OpenHyperlink; @@ -295,7 +294,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation private: struct SharedState { - std::unique_ptr> updatePatternLocations; + std::unique_ptr> outputIdle; std::shared_ptr> updateScrollBar; }; @@ -376,7 +375,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation const int bufferSize); void _terminalTaskbarProgressChanged(); void _terminalShowWindowChanged(bool showOrHide); - void _terminalTextLayoutUpdated(); void _terminalPlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 038f21c0f4..ac5eed7345 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -49,6 +49,13 @@ namespace Microsoft.Terminal.Control Boolean EndAtRightBoundary; }; + struct SearchResults + { + Int32 TotalMatches; + Int32 CurrentMatch; + Boolean SearchInvalidated; + }; + [default_interface] runtimeclass SelectionColor { SelectionColor(); @@ -127,7 +134,7 @@ namespace Microsoft.Terminal.Control void ResumeRendering(); void BlinkAttributeTick(); - void Search(String text, Boolean goForward, Boolean caseSensitive); + SearchResults Search(String text, Boolean goForward, Boolean caseSensitive, Boolean reset); void ClearSearch(); IVector SearchResultRows { get; }; Boolean SnapSearchResultToSelection; @@ -177,8 +184,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler RendererWarning; event Windows.Foundation.TypedEventHandler RaiseNotice; event Windows.Foundation.TypedEventHandler TransparencyChanged; - event Windows.Foundation.TypedEventHandler ReceivedOutput; - event Windows.Foundation.TypedEventHandler UpdateSearchResults; + event Windows.Foundation.TypedEventHandler OutputIdle; event Windows.Foundation.TypedEventHandler UpdateSelectionMarkers; event Windows.Foundation.TypedEventHandler OpenHyperlink; event Windows.Foundation.TypedEventHandler CloseTerminalRequested; diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 4c4d169095..fb69e9e054 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -12,7 +12,6 @@ #include "ScrollPositionChangedArgs.g.cpp" #include "RendererWarningArgs.g.cpp" #include "TransparencyChangedEventArgs.g.cpp" -#include "UpdateSearchResultsEventArgs.g.cpp" #include "ShowWindowArgs.g.cpp" #include "UpdateSelectionMarkersEventArgs.g.cpp" #include "CompletionsChangedEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index d8990c6f21..423e5695d7 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -12,7 +12,6 @@ #include "ScrollPositionChangedArgs.g.h" #include "RendererWarningArgs.g.h" #include "TransparencyChangedEventArgs.g.h" -#include "UpdateSearchResultsEventArgs.g.h" #include "ShowWindowArgs.g.h" #include "UpdateSelectionMarkersEventArgs.g.h" #include "CompletionsChangedEventArgs.g.h" @@ -141,17 +140,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(float, Opacity); }; - struct UpdateSearchResultsEventArgs : public UpdateSearchResultsEventArgsT - { - public: - UpdateSearchResultsEventArgs() = default; - - WINRT_PROPERTY(SearchState, State, SearchState::Inactive); - WINRT_PROPERTY(bool, FoundMatch); - WINRT_PROPERTY(int32_t, TotalMatches); - WINRT_PROPERTY(int32_t, CurrentMatch); - }; - struct ShowWindowArgs : public ShowWindowArgsT { public: diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index d04044073e..6b0bf11e35 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -84,14 +84,6 @@ namespace Microsoft.Terminal.Control Active = 1, }; - runtimeclass UpdateSearchResultsEventArgs - { - SearchState State { get; }; - Boolean FoundMatch { get; }; - Int32 TotalMatches { get; }; - Int32 CurrentMatch { get; }; - } - runtimeclass ShowWindowArgs { Boolean ShowOrHide { get; }; diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index 9ce4bba86a..eaaae04add 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -195,6 +195,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } + winrt::hstring SearchBoxControl::Text() + { + return TextBox().Text(); + } + // Method Description: // - Check if the current search direction is forward // Arguments: @@ -202,7 +207,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Return Value: // - bool: the current search direction, determined by the // states of the two direction buttons - bool SearchBoxControl::_GoForward() + bool SearchBoxControl::GoForward() { return GoForwardButton().IsChecked().GetBoolean(); } @@ -214,7 +219,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Return Value: // - bool: whether the current search is case sensitive (case button is checked ) // or not - bool SearchBoxControl::_CaseSensitive() + bool SearchBoxControl::CaseSensitive() { return CaseSensitivityButton().IsChecked().GetBoolean(); } @@ -240,11 +245,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift); if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down)) { - Search.raise(TextBox().Text(), !_GoForward(), _CaseSensitive()); + Search.raise(Text(), !GoForward(), CaseSensitive()); } else { - Search.raise(TextBox().Text(), _GoForward(), _CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive()); } e.Handled(true); } @@ -335,7 +340,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // kick off search - Search.raise(TextBox().Text(), _GoForward(), _CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive()); } // Method Description: @@ -356,7 +361,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // kick off search - Search.raise(TextBox().Text(), _GoForward(), _CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive()); } // Method Description: @@ -394,7 +399,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void SearchBoxControl::TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) { - SearchChanged.raise(TextBox().Text(), _GoForward(), _CaseSensitive()); + SearchChanged.raise(Text(), GoForward(), CaseSensitive()); } // Method Description: @@ -406,7 +411,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void SearchBoxControl::CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) { - SearchChanged.raise(TextBox().Text(), _GoForward(), _CaseSensitive()); + SearchChanged.raise(Text(), GoForward(), CaseSensitive()); } // Method Description: @@ -528,20 +533,4 @@ namespace winrt::Microsoft::Terminal::Control::implementation { StatusBox().Text(L""); } - - // Method Description: - // - Enables / disables results navigation buttons - // Arguments: - // - enable: if true, the buttons should be enabled - // Return Value: - // - - void SearchBoxControl::NavigationEnabled(bool enabled) - { - GoBackwardButton().IsEnabled(enabled); - GoForwardButton().IsEnabled(enabled); - } - bool SearchBoxControl::NavigationEnabled() - { - return GoBackwardButton().IsEnabled() || GoForwardButton().IsEnabled(); - } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 53a738c399..60ea00da48 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -35,13 +35,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation void Open(std::function callback); void Close(); + winrt::hstring Text(); + bool GoForward(); + bool CaseSensitive(); void SetFocusOnTextbox(); void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); void SetStatus(int32_t totalMatches, int32_t currentMatch); void ClearStatus(); - bool NavigationEnabled(); - void NavigationEnabled(bool enabled); void GoBackwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); void GoForwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); @@ -77,8 +78,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation static double _TextWidth(winrt::hstring text, double fontSize); double _GetStatusMaxWidth(); - bool _GoForward(); - bool _CaseSensitive(); void _KeyDownHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); void _CharacterHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::Input::CharacterReceivedRoutedEventArgs& e); }; diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl index 42abd1fa1b..3abf4f5022 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.idl +++ b/src/cascadia/TerminalControl/SearchBoxControl.idl @@ -16,8 +16,6 @@ namespace Microsoft.Terminal.Control Windows.Foundation.Rect ContentClipRect{ get; }; Double OpenAnimationStartPoint{ get; }; - Boolean NavigationEnabled; - event SearchHandler Search; event SearchHandler SearchChanged; event Windows.Foundation.TypedEventHandler Closed; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 81934f10f6..22efc0b18e 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -204,7 +204,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _revokers.TransparencyChanged = _core.TransparencyChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreTransparencyChanged }); _revokers.RaiseNotice = _core.RaiseNotice(winrt::auto_revoke, { get_weak(), &TermControl::_coreRaisedNotice }); _revokers.HoveredHyperlinkChanged = _core.HoveredHyperlinkChanged(winrt::auto_revoke, { get_weak(), &TermControl::_hoveredHyperlinkChanged }); - _revokers.UpdateSearchResults = _core.UpdateSearchResults(winrt::auto_revoke, { get_weak(), &TermControl::_coreUpdateSearchResults }); + _revokers.OutputIdle = _core.OutputIdle(winrt::auto_revoke, { get_weak(), &TermControl::_coreOutputIdle }); _revokers.UpdateSelectionMarkers = _core.UpdateSelectionMarkers(winrt::auto_revoke, { get_weak(), &TermControl::_updateSelectionMarkers }); _revokers.coreOpenHyperlink = _core.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler }); _revokers.interactivityOpenHyperlink = _interactivity.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler }); @@ -534,13 +534,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation // but since code paths differ, extra work is required to ensure correctness. if (!_core.HasMultiLineSelection()) { - _core.SnapSearchResultToSelection(true); const auto selectedLine{ _core.SelectedText(true) }; _searchBox->PopulateTextbox(selectedLine); } } - _searchBox->Open([searchBox]() { searchBox.SetFocusOnTextbox(); }); + _searchBox->Open([weakThis = get_weak()]() { + if (const auto self = weakThis.get(); !self->_IsClosing()) + { + self->_searchBox->SetFocusOnTextbox(); + self->_refreshSearch(); + } + }); } } } @@ -551,13 +556,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation { return; } - if (!_searchBox) + if (!_searchBox || _searchBox->Visibility() != Visibility::Visible) { CreateSearchBoxControl(); } else { - _core.Search(_searchBox->TextBox().Text(), goForward, false); + _handleSearchResults(_core.Search(_searchBox->Text(), goForward, _searchBox->CaseSensitive(), false)); } } @@ -588,7 +593,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const bool goForward, const bool caseSensitive) { - _core.Search(text, goForward, caseSensitive); + _handleSearchResults(_core.Search(text, goForward, caseSensitive, false)); } // Method Description: @@ -605,7 +610,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (_searchBox && _searchBox->Visibility() == Visibility::Visible) { - _core.Search(text, goForward, caseSensitive); + _handleSearchResults(_core.Search(text, goForward, caseSensitive, false)); } } @@ -621,6 +626,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const RoutedEventArgs& /*args*/) { _searchBox->Close(); + _core.ClearSearch(); // Set focus back to terminal control this->Focus(FocusState::Programmatic); @@ -3537,69 +3543,63 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _core.SelectedText(trimTrailingWhitespace); } - // Method Description: - // - Triggers an update on scrollbar to redraw search scroll marks. - // - Called when the search results have changed, and the scroll marks' - // positions need to be updated. - void TermControl::_UpdateSearchScrollMarks() + void TermControl::_refreshSearch() { - // Manually send a scrollbar update, on the UI thread. We're already - // UI-driven, so that's okay. We're not really changing the scrollbar, - // but we do want to update the position of any search marks. The Core - // might send a scrollbar update event too, but if the first search hit - // is in the visible viewport, then the pips won't display until the - // user first scrolls. - auto scrollBar = ScrollBar(); - ScrollBarUpdate update{ - .newValue = scrollBar.Value(), - .newMaximum = scrollBar.Maximum(), - .newMinimum = scrollBar.Minimum(), - .newViewportSize = scrollBar.ViewportSize(), - }; - _throttledUpdateScrollbar(update); + if (!_searchBox || _searchBox->Visibility() != Visibility::Visible) + { + return; + } + + const auto text = _searchBox->Text(); + if (text.empty()) + { + return; + } + + const auto goForward = _searchBox->GoForward(); + const auto caseSensitive = _searchBox->CaseSensitive(); + _handleSearchResults(_core.Search(text, goForward, caseSensitive, true)); } - // Method Description: - // - Called when the core raises a UpdateSearchResults event. That's done in response to: - // - starting a search query with ControlCore::Search. - // - clearing search results due to change in buffer content. - // - The args will tell us if there were or were not any results for that - // particular search. We'll use that to control what to announce to - // Narrator. When we have more elaborate search information to report, we - // may want to report that here. (see GH #3920) - // Arguments: - // - args: contains information about the search state and results that were - // or were not found. - // Return Value: - // - - winrt::fire_and_forget TermControl::_coreUpdateSearchResults(const IInspectable& /*sender*/, Control::UpdateSearchResultsEventArgs args) + void TermControl::_handleSearchResults(SearchResults results) { - co_await wil::resume_foreground(Dispatcher()); - if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) }) + if (!_searchBox) { - automationPeer.RaiseNotificationEvent( - Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - args.FoundMatch() ? RS_(L"SearchBox_MatchesAvailable") : RS_(L"SearchBox_NoMatches"), // what to announce if results were found - L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */); + return; } - _UpdateSearchScrollMarks(); + _searchBox->SetStatus(results.TotalMatches, results.CurrentMatch); - if (_searchBox) + if (results.SearchInvalidated) { - _searchBox->NavigationEnabled(true); - if (args.State() == Control::SearchState::Inactive) + if (_showMarksInScrollbar) { - _searchBox->ClearStatus(); + const auto scrollBar = ScrollBar(); + ScrollBarUpdate update{ + .newValue = scrollBar.Value(), + .newMaximum = scrollBar.Maximum(), + .newMinimum = scrollBar.Minimum(), + .newViewportSize = scrollBar.ViewportSize(), + }; + _updateScrollBar->Run(update); } - else + + if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) }) { - _searchBox->SetStatus(args.TotalMatches(), args.CurrentMatch()); + automationPeer.RaiseNotificationEvent( + AutomationNotificationKind::ActionCompleted, + AutomationNotificationProcessing::ImportantMostRecent, + results.TotalMatches > 0 ? RS_(L"SearchBox_MatchesAvailable") : RS_(L"SearchBox_NoMatches"), // what to announce if results were found + L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */); } } } + void TermControl::_coreOutputIdle(const IInspectable& /*sender*/, const IInspectable& /*args*/) + { + _refreshSearch(); + } + void TermControl::OwningHwnd(uint64_t owner) { _core.OwningHwnd(owner); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 8568cb49b0..4b7c4a5078 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -371,10 +371,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation double _GetAutoScrollSpeed(double cursorDistanceFromBorder) const; void _Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); - void _SearchChanged(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); - void _UpdateSearchScrollMarks(); + void _refreshSearch(); + void _handleSearchResults(SearchResults results); void _hoveredHyperlinkChanged(const IInspectable& sender, const IInspectable& args); winrt::fire_and_forget _updateSelectionMarkers(IInspectable sender, Control::UpdateSelectionMarkersEventArgs args); @@ -383,7 +383,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); - winrt::fire_and_forget _coreUpdateSearchResults(const IInspectable& sender, Control::UpdateSearchResultsEventArgs args); + void _coreOutputIdle(const IInspectable& sender, const IInspectable& args); til::point _toPosInDips(const Core::Point terminalCellPos); void _throttledUpdateScrollbar(const ScrollBarUpdate& update); @@ -411,7 +411,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::ControlCore::TransparencyChanged_revoker TransparencyChanged; Control::ControlCore::RaiseNotice_revoker RaiseNotice; Control::ControlCore::HoveredHyperlinkChanged_revoker HoveredHyperlinkChanged; - Control::ControlCore::UpdateSearchResults_revoker UpdateSearchResults; + Control::ControlCore::OutputIdle_revoker OutputIdle; Control::ControlCore::UpdateSelectionMarkers_revoker UpdateSelectionMarkers; Control::ControlCore::OpenHyperlink_revoker coreOpenHyperlink; Control::ControlCore::TitleChanged_revoker TitleChanged; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 25ea33fd47..854d54394d 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -1230,11 +1230,6 @@ void Microsoft::Terminal::Core::Terminal::CompletionsChangedCallback(std::functi _pfnCompletionsChanged.swap(pfn); } -void Terminal::SetTextLayoutUpdatedCallback(std::function pfn) noexcept -{ - _pfnTextLayoutUpdated.swap(pfn); -} - // Method Description: // - Stores the search highlighted regions in the terminal void Terminal::SetSearchHighlights(const std::vector& highlights) noexcept diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 7f1c20789e..75b822b138 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -155,7 +155,6 @@ public: bool IsVtInputEnabled() const noexcept override; void NotifyAccessibilityChange(const til::rect& changedRect) noexcept override; void NotifyBufferRotation(const int delta) override; - void NotifyTextLayoutUpdated() override; void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; @@ -231,7 +230,6 @@ public: void SetShowWindowCallback(std::function pfn) noexcept; void SetPlayMidiNoteCallback(std::function pfn) noexcept; void CompletionsChangedCallback(std::function pfn) noexcept; - void SetTextLayoutUpdatedCallback(std::function pfn) noexcept; void SetSearchHighlights(const std::vector& highlights) noexcept; void SetSearchHighlightFocused(const size_t focusedIdx); @@ -341,7 +339,6 @@ private: std::function _pfnShowWindowChanged; std::function _pfnPlayMidiNote; std::function _pfnCompletionsChanged; - std::function _pfnTextLayoutUpdated; RenderSettings _renderSettings; std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 07c81649b7..42f1c903c3 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -239,8 +239,6 @@ void Terminal::UseAlternateScreenBuffer(const TextAttribute& attrs) // Update scrollbars _NotifyScrollEvent(); - NotifyTextLayoutUpdated(); - // redraw the screen try { @@ -298,8 +296,6 @@ void Terminal::UseMainScreenBuffer() // Update scrollbars _NotifyScrollEvent(); - NotifyTextLayoutUpdated(); - // redraw the screen _activeBuffer().TriggerRedrawAll(); } @@ -374,15 +370,3 @@ void Terminal::NotifyBufferRotation(const int delta) _NotifyScrollEvent(); } } - -// Method Description: -// - Notifies the terminal UI layer that the text layout has changed. -// - This will be called when new text is added, or when the text is -// rearranged in the buffer due to window resize. -void Terminal::NotifyTextLayoutUpdated() -{ - if (_pfnTextLayoutUpdated) - { - _pfnTextLayoutUpdated(); - } -} diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 25a2f1fdb2..fb20d74b75 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -424,12 +424,7 @@ void ConhostInternalGetSet::NotifyBufferRotation(const int delta) } } -void ConhostInternalGetSet::NotifyTextLayoutUpdated() -{ - // Not implemented for conhost. -} - void ConhostInternalGetSet::InvokeCompletions(std::wstring_view /*menuJson*/, unsigned int /*replaceLength*/) { // Not implemented for conhost. -} \ No newline at end of file +} diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index ca5711f638..01d8abaf17 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -68,7 +68,6 @@ public: void NotifyAccessibilityChange(const til::rect& changedRect) override; void NotifyBufferRotation(const int delta) override; - void NotifyTextLayoutUpdated() override; void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; diff --git a/src/inc/til/throttled_func.h b/src/inc/til/throttled_func.h index 01c4e356aa..d250f475af 100644 --- a/src/inc/til/throttled_func.h +++ b/src/inc/til/throttled_func.h @@ -81,7 +81,7 @@ namespace til }; } // namespace details - template + template class throttled_func { public: @@ -118,15 +118,35 @@ namespace til throttled_func& operator=(throttled_func&&) = delete; // Throttles the invocation of the function passed to the constructor. - // If this is a trailing_throttled_func: - // If you call this function again before the underlying - // timer has expired, the new arguments will be used. + // + // If Debounce is true and you call this function again before the + // underlying timer has expired, its timeout will be reset. + // + // If Leading is true and you call this function again before the + // underlying timer has expired, the new arguments will be used. template void operator()(MakeArgs&&... args) { - if (!_storage.emplace(std::forward(args)...)) + const auto hadValue = _storage.emplace(std::forward(args)...); + + if constexpr (Debounce) { - _leading_edge(); + SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0); + } + else + { + if (!hadValue) + { + SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0); + } + } + + if constexpr (Leading) + { + if (!hadValue) + { + _func(); + } } } @@ -165,19 +185,9 @@ namespace til } CATCH_LOG() - void _leading_edge() - { - if constexpr (leading) - { - _func(); - } - - SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0); - } - void _trailing_edge() { - if constexpr (leading) + if constexpr (Leading) { _storage.reset(); } @@ -187,7 +197,7 @@ namespace til } } - inline wil::unique_threadpool_timer _createTimer() + wil::unique_threadpool_timer _createTimer() { wil::unique_threadpool_timer timer{ CreateThreadpoolTimer(&_timer_callback, this, nullptr) }; THROW_LAST_ERROR_IF(!timer); @@ -201,6 +211,10 @@ namespace til }; template - using throttled_func_trailing = throttled_func; - using throttled_func_leading = throttled_func; + using throttled_func_trailing = throttled_func; + using throttled_func_leading = throttled_func; + + template + using debounced_func_trailing = throttled_func; + using debounced_func_leading = throttled_func; } // namespace til diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 9ef05279bf..674dc3baf7 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -5,6 +5,7 @@ #include "AtlasEngine.h" #include "Backend.h" +#include "../../buffer/out/textBuffer.hpp" #include "../base/FontCache.h" // #### NOTE #### @@ -95,7 +96,7 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2& out) noexcept return S_OK; } -[[nodiscard]] HRESULT AtlasEngine::InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept +[[nodiscard]] HRESULT AtlasEngine::InvalidateHighlight(std::span highlights, const TextBuffer& buffer) noexcept { const auto viewportOrigin = til::point{ _api.s->viewportOffset.x, _api.s->viewportOffset.y }; const auto viewport = til::rect{ 0, 0, _api.s->viewportCellCount.x, _api.s->viewportCellCount.y }; @@ -103,7 +104,7 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2& out) noexcept for (const auto& hi : highlights) { hi.iterate_rows(cellCountX, [&](til::CoordType row, til::CoordType beg, til::CoordType end) { - const auto shift = til::at(renditions, row) != LineRendition::SingleWidth ? 1 : 0; + const auto shift = buffer.GetLineRendition(row) != LineRendition::SingleWidth ? 1 : 0; beg <<= shift; end <<= shift; til::rect rect{ beg, row, end + 1, row + 1 }; diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 9f38b7094f..9ad4425bea 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -33,7 +33,7 @@ namespace Microsoft::Console::Render::Atlas [[nodiscard]] HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept override; [[nodiscard]] HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; - [[nodiscard]] HRESULT InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept override; + [[nodiscard]] HRESULT InvalidateHighlight(std::span highlights, const TextBuffer& buffer) noexcept override; [[nodiscard]] HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept override; [[nodiscard]] HRESULT InvalidateAll() noexcept override; [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override; diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index 6ab523a1a4..1847c1d287 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -7,7 +7,7 @@ using namespace Microsoft::Console; using namespace Microsoft::Console::Render; -[[nodiscard]] HRESULT RenderEngineBase::InvalidateHighlight(std::span /*highlights*/, const std::vector& /*renditions*/) noexcept +[[nodiscard]] HRESULT RenderEngineBase::InvalidateHighlight(std::span /*highlights*/, const TextBuffer& /*renditions*/) noexcept { return S_OK; } diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index deb8925c94..82b1eaea1b 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -459,24 +459,21 @@ void Renderer::TriggerSelection() void Renderer::TriggerSearchHighlight(const std::vector& oldHighlights) try { - const auto& buffer = _pData->GetTextBuffer(); - const auto rows = buffer.TotalRowCount(); - - std::vector renditions; - renditions.reserve(rows); - for (til::CoordType row = 0; row < rows; ++row) - { - renditions.emplace_back(buffer.GetLineRendition(row)); - } - // no need to invalidate focused search highlight separately as they are // included in (all) search highlights. const auto newHighlights = _pData->GetSearchHighlights(); + if (oldHighlights.empty() && newHighlights.empty()) + { + return; + } + + const auto& buffer = _pData->GetTextBuffer(); + FOREACH_ENGINE(pEngine) { - LOG_IF_FAILED(pEngine->InvalidateHighlight(oldHighlights, renditions)); - LOG_IF_FAILED(pEngine->InvalidateHighlight(newHighlights, renditions)); + LOG_IF_FAILED(pEngine->InvalidateHighlight(oldHighlights, buffer)); + LOG_IF_FAILED(pEngine->InvalidateHighlight(newHighlights, buffer)); } NotifyPaintFrame(); diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 7533de8a4d..a7afb4244b 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -67,7 +67,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateSelection(const std::vector& rectangles) noexcept = 0; - [[nodiscard]] virtual HRESULT InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept = 0; + [[nodiscard]] virtual HRESULT InvalidateHighlight(std::span highlights, const TextBuffer& buffer) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateAll() noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept = 0; diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 5fd8a94c8a..1108921edb 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -24,7 +24,7 @@ namespace Microsoft::Console::Render class RenderEngineBase : public IRenderEngine { public: - [[nodiscard]] HRESULT InvalidateHighlight(std::span highlights, const std::vector& renditions) noexcept override; + [[nodiscard]] HRESULT InvalidateHighlight(std::span highlights, const TextBuffer& buffer) noexcept override; [[nodiscard]] HRESULT InvalidateTitle(const std::wstring_view proposedTitle) noexcept override; [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override; diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index b42ffe283f..4381c4ecbd 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -80,7 +80,6 @@ namespace Microsoft::Console::VirtualTerminal virtual void NotifyAccessibilityChange(const til::rect& changedRect) = 0; virtual void NotifyBufferRotation(const int delta) = 0; - virtual void NotifyTextLayoutUpdated() = 0; virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0; }; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 1a887f99f8..34bc5cd80e 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -182,7 +182,6 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) // It's important to do this here instead of in TextBuffer, because here you // have access to the entire line of text, whereas TextBuffer writes it one // character at a time via the OutputCellIterator. - _api.NotifyTextLayoutUpdated(); textBuffer.TriggerNewTextNotification(string); } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index b17bc4429f..5cd5fbdc29 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -215,11 +215,6 @@ public: Log::Comment(L"NotifyBufferRotation MOCK called..."); } - void NotifyTextLayoutUpdated() override - { - Log::Comment(L"NotifyTextLayoutUpdated MOCK called..."); - } - void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override { Log::Comment(L"InvokeCompletions MOCK called..."); From ce4e0df7b0da5549a69219cd3e866b1bda1c2fbd Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Tue, 23 Apr 2024 17:17:08 -0700 Subject: [PATCH 15/53] Update Azure Cloud Shell API to the newer version (#17115) Updates the `api-version` to `2023-02-01-preview` when requesting for CloudShell settings and shell ## Validation Steps Performed Can still use Azure Cloud Shell through Windows Terminal --- src/cascadia/TerminalConnection/AzureConnection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalConnection/AzureConnection.cpp b/src/cascadia/TerminalConnection/AzureConnection.cpp index 973ade12d6..e633e9b80c 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.cpp +++ b/src/cascadia/TerminalConnection/AzureConnection.cpp @@ -962,7 +962,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - the user's cloud shell settings WDJ::JsonObject AzureConnection::_GetCloudShellUserSettings() { - auto uri{ fmt::format(L"{}providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2020-04-01-preview", _resourceUri) }; + auto uri{ fmt::format(L"{}providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2023-02-01-preview", _resourceUri) }; return _SendRequestReturningJson(uri, nullptr); } @@ -972,7 +972,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - the uri for the cloud shell winrt::hstring AzureConnection::_GetCloudShell() { - auto uri{ fmt::format(L"{}providers/Microsoft.Portal/consoles/default?api-version=2020-04-01-preview", _resourceUri) }; + auto uri{ fmt::format(L"{}providers/Microsoft.Portal/consoles/default?api-version=2023-02-01-preview", _resourceUri) }; WWH::HttpStringContent content{ LR"-({"properties": {"osType": "linux"}})-", From 3a63832c31c3e4ef7b22169216a5a14607efe3eb Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 24 Apr 2024 12:28:02 -0500 Subject: [PATCH 16/53] [REPLAY] Move to AzureFileCopy@6 for Managed Identity support (#17121) This is required for us to move off Entra ID Application identity. (cherry picked from commit 2e7c3fa3132fa3bff23255480c593cbd3432eee5) This was approved in #16957, so I will merge with one signoff. --- build/pipelines/templates-v2/job-deploy-to-azure-storage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml b/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml index 69b82e57bb..e2ed380fd4 100644 --- a/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml +++ b/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml @@ -80,7 +80,7 @@ jobs: Install-Module -Verbose -AllowClobber -Force Az.Accounts, Az.Storage, Az.Network, Az.Resources, Az.Compute displayName: Install Azure Module Dependencies - - task: AzureFileCopy@5 + - task: AzureFileCopy@6 displayName: Publish to Storage Account inputs: sourcePath: _out/* From 19f43f70bdbf6ea3d3c575b6a532cae2ee5ca6a2 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 24 Apr 2024 13:00:57 -0500 Subject: [PATCH 17/53] build: disable CheckCFlags for now, as it is blowing up the build (#17116) OneBranch no likey. A test build is running now. --- .github/actions/spelling/allow/microsoft.txt | 2 ++ .../pipeline-onebranch-full-release-build.yml | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 9cda69aa94..876d9fd960 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -14,6 +14,7 @@ autoexec backplating bitmaps BOMs +checkcflags COMPUTERNAME CPLs cpptools @@ -101,3 +102,4 @@ wtl wtt wttlog Xamarin +xfgcheck diff --git a/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml index 37d21c89a6..9fc9df3068 100644 --- a/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml +++ b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml @@ -82,6 +82,7 @@ extends: cloudvault: # https://aka.ms/obpipelines/cloudvault enabled: false globalSdl: # https://aka.ms/obpipelines/sdl + enableCheckCFlags: false # CheckCFlags is broken and exploding our builds; to remove, :g/BAD-FLAGS/d asyncSdl: enabled: true tsaOptionsFile: 'build/config/tsa.json' @@ -107,6 +108,8 @@ extends: parameters: pool: { type: windows } variables: + ob_sdl_checkcflags_enabled: false # BAD-FLAGS + ob_sdl_xfgcheck_enabled: false # BAD-FLAGS ob_git_checkout: false # This job checks itself out ob_git_skip_checkout_none: true ob_outputDirectory: $(JobOutputDirectory) @@ -141,6 +144,8 @@ extends: parameters: pool: { type: windows } variables: + ob_sdl_checkcflags_enabled: false # BAD-FLAGS + ob_sdl_xfgcheck_enabled: false # BAD-FLAGS ob_git_checkout: false # This job checks itself out ob_git_skip_checkout_none: true ob_outputDirectory: $(JobOutputDirectory) @@ -172,6 +177,8 @@ extends: parameters: pool: { type: windows } variables: + ob_sdl_checkcflags_enabled: false # BAD-FLAGS + ob_sdl_xfgcheck_enabled: false # BAD-FLAGS ob_git_checkout: false # This job checks itself out ob_git_skip_checkout_none: true ob_outputDirectory: $(JobOutputDirectory) @@ -223,6 +230,8 @@ extends: parameters: pool: { type: windows } variables: + ob_sdl_checkcflags_enabled: false # BAD-FLAGS + ob_sdl_xfgcheck_enabled: false # BAD-FLAGS ob_git_checkout: false # This job checks itself out ob_git_skip_checkout_none: true ob_outputDirectory: $(JobOutputDirectory) @@ -238,6 +247,8 @@ extends: parameters: pool: { type: windows } variables: + ob_sdl_checkcflags_enabled: false # BAD-FLAGS + ob_sdl_xfgcheck_enabled: false # BAD-FLAGS ob_git_checkout: false # This job checks itself out ob_git_skip_checkout_none: true ob_outputDirectory: $(JobOutputDirectory) @@ -260,6 +271,8 @@ extends: subscription: ${{ parameters.symbolPublishingSubscription }} symbolProject: ${{ parameters.symbolPublishingProject }} variables: + ob_sdl_checkcflags_enabled: false # BAD-FLAGS + ob_sdl_xfgcheck_enabled: false # BAD-FLAGS ob_git_checkout: false # This job checks itself out ob_git_skip_checkout_none: true ob_outputDirectory: $(Build.ArtifactStagingDirectory) From 0c3c7470b084ce5d5a3d76dfeb4c56d070ac761d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 24 Apr 2024 23:23:50 +0200 Subject: [PATCH 18/53] Add a system message to session restore (#17113) This adds a system message which displays the time at which the buffer snapshot was written to disk. Additionally, this PR moves the snapshot loading into a background thread, so that the UI thread is unblocked and that multiple tabs/panes can load simultaneously. Closes #17031 Closes #17074 ## Validation Steps Performed Repeatedly closing and opening WT adds more and more messages. Currently, the messages get somewhat corrupted due to a bug in our line-wrap handling, or some similar part. --- .github/actions/spelling/expect/expect.txt | 1 + src/cascadia/TerminalControl/ControlCore.cpp | 46 ++++++++++++++++-- .../Resources/en-US/Resources.resw | 4 ++ src/cascadia/TerminalControl/TermControl.cpp | 47 +++++++++++++++++-- src/cascadia/TerminalControl/TermControl.h | 1 + 5 files changed, 92 insertions(+), 7 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 36e345cfbb..775694924d 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1718,6 +1718,7 @@ sysparams sysparamsext SYSTEMHAND SYSTEMMENU +SYSTEMTIME tabview TAdd taef diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 1e82b825eb..da46f21b65 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1777,6 +1777,38 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } + FILETIME lastWriteTime; + SYSTEMTIME lastWriteSystemTime; + if (!GetFileTime(file.get(), nullptr, nullptr, &lastWriteTime) || + !FileTimeToSystemTime(&lastWriteTime, &lastWriteSystemTime)) + { + return; + } + + wchar_t dateBuf[256]; + const auto dateLen = GetDateFormatEx(nullptr, 0, &lastWriteSystemTime, nullptr, &dateBuf[0], ARRAYSIZE(dateBuf), nullptr); + wchar_t timeBuf[256]; + const auto timeLen = GetTimeFormatEx(nullptr, 0, &lastWriteSystemTime, nullptr, &timeBuf[0], ARRAYSIZE(timeBuf)); + + std::wstring message; + if (dateLen > 0 && timeLen > 0) + { + const auto msg = RS_(L"SessionRestoreMessage"); + const std::wstring_view date{ &dateBuf[0], gsl::narrow_cast(dateLen) }; + const std::wstring_view time{ &timeBuf[0], gsl::narrow_cast(timeLen) }; + // This escape sequence string + // * sets the color to white on a bright black background ("\x1b[100;37m") + // * prints " [Restored + + Restored + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 22efc0b18e..6502be591e 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1187,11 +1187,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (!_restorePath.empty()) { - winrt::get_self(_core)->RestoreFromPath(_restorePath.c_str()); - _restorePath = {}; + _restoreInBackground(); + } + else + { + _core.Connection().Start(); } - - _core.Connection().Start(); } else { @@ -1278,6 +1279,44 @@ namespace winrt::Microsoft::Terminal::Control::implementation return true; } + winrt::fire_and_forget TermControl::_restoreInBackground() + { + const auto path = std::exchange(_restorePath, {}); + const auto weakSelf = get_weak(); + winrt::apartment_context uiThread; + + try + { + co_await winrt::resume_background(); + + const auto self = weakSelf.get(); + if (!self) + { + co_return; + } + + winrt::get_self(_core)->RestoreFromPath(path.c_str()); + } + CATCH_LOG(); + + try + { + co_await uiThread; + + const auto self = weakSelf.get(); + if (!self) + { + co_return; + } + + if (const auto connection = _core.Connection()) + { + connection.Start(); + } + } + CATCH_LOG(); + } + void TermControl::_CharacterHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, const Input::CharacterReceivedRoutedEventArgs& e) { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 4b7c4a5078..66995a92c6 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -319,6 +319,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Reattach }; bool _InitializeTerminal(const InitializeReason reason); + winrt::fire_and_forget _restoreInBackground(); void _SetFontSize(int fontSize); void _TappedHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e); void _KeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); From f36d589a8eb60fe9ff4c8efaad8eac088647e8a8 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 25 Apr 2024 04:55:28 -0700 Subject: [PATCH 19/53] Fix persisting the size of a focus mode window (#17068) While I was fixing the initial position thing, I figured I'd fix this too. We were mistakenly accounting for the size of the titlebar when we should launch into focus mode (without one) Closes #10730 --- src/cascadia/TerminalApp/TerminalWindow.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index bd246bb53d..749fd2e842 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -562,9 +562,21 @@ namespace winrt::TerminalApp::implementation { winrt::Windows::Foundation::Size proposedSize{}; + // In focus mode, we don't want to include our own tab row in the size + // of the window that we hand back. So we account for passing + // --focusMode on the commandline here, and the mode in the settings. + // Below, we'll also account for if focus mode was persisted into the + // session for restoration. + bool focusMode = _appArgs.GetLaunchMode().value_or(_settings.GlobalSettings().LaunchMode()) == LaunchMode::FocusMode; + const auto scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); if (const auto layout = LoadPersistedLayout()) { + if (layout.LaunchMode()) + { + focusMode = layout.LaunchMode().Value() == LaunchMode::FocusMode; + } + if (layout.InitialSize()) { proposedSize = layout.InitialSize().Value(); @@ -602,7 +614,7 @@ namespace winrt::TerminalApp::implementation // GH#2061 - If the global setting "Always show tab bar" is // set or if "Show tabs in title bar" is set, then we'll need to add // the height of the tab bar here. - if (_settings.GlobalSettings().ShowTabsInTitlebar()) + if (_settings.GlobalSettings().ShowTabsInTitlebar() && !focusMode) { // In the past, we used to actually instantiate a TitlebarControl // and use Measure() to determine the DesiredSize of the control, to @@ -620,7 +632,7 @@ namespace winrt::TerminalApp::implementation static constexpr auto titlebarHeight = 40; proposedSize.Height += (titlebarHeight)*scale; } - else if (_settings.GlobalSettings().AlwaysShowTabs()) + else if (_settings.GlobalSettings().AlwaysShowTabs() && !focusMode) { // Same comment as above, but with a TabRowControl. // From 26900ca4726ffe11f3047ba626e22ea84ff35c0b Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:16:02 -0500 Subject: [PATCH 20/53] Localization Updates - main - 04/25/2024 03:04:34 (#17125) --- src/cascadia/TerminalControl/Resources/de-DE/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/es-ES/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/fr-FR/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/it-IT/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/ja-JP/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/ko-KR/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/pt-BR/Resources.resw | 4 ++++ .../TerminalControl/Resources/qps-ploc/Resources.resw | 4 ++++ .../TerminalControl/Resources/qps-ploca/Resources.resw | 4 ++++ .../TerminalControl/Resources/qps-plocm/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/ru-RU/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/zh-CN/Resources.resw | 4 ++++ src/cascadia/TerminalControl/Resources/zh-TW/Resources.resw | 4 ++++ 13 files changed, 52 insertions(+) diff --git a/src/cascadia/TerminalControl/Resources/de-DE/Resources.resw b/src/cascadia/TerminalControl/Resources/de-DE/Resources.resw index 338cb51d30..2239b0f54f 100644 --- a/src/cascadia/TerminalControl/Resources/de-DE/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/de-DE/Resources.resw @@ -296,4 +296,8 @@ Installieren Sie entweder die fehlende Schriftart, oder wählen Sie eine andere Ausgabe auswählen The tooltip for a button for selecting all of a command's output + + Wiederhergestellt + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/es-ES/Resources.resw b/src/cascadia/TerminalControl/Resources/es-ES/Resources.resw index 7ecef8b922..8d20efb990 100644 --- a/src/cascadia/TerminalControl/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/es-ES/Resources.resw @@ -296,4 +296,8 @@ Instale la fuente que falta o elija otra. Seleccionar salida The tooltip for a button for selecting all of a command's output + + Se ha restaurado + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/fr-FR/Resources.resw b/src/cascadia/TerminalControl/Resources/fr-FR/Resources.resw index 6f3e367fdc..9167367c36 100644 --- a/src/cascadia/TerminalControl/Resources/fr-FR/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/fr-FR/Resources.resw @@ -296,4 +296,8 @@ Installez la police manquante ou choisissez-en une autre. Sélectionner la sortie The tooltip for a button for selecting all of a command's output + + Restaurée + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw b/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw index 39a76ac191..3ebb2a21cb 100644 --- a/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw @@ -296,4 +296,8 @@ Installare il tipo di carattere mancante o sceglierne un altro. Seleziona output The tooltip for a button for selecting all of a command's output + + Ripristinato + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalControl/Resources/ja-JP/Resources.resw index b352afeeec..a3cdeb2f97 100644 --- a/src/cascadia/TerminalControl/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/ja-JP/Resources.resw @@ -296,4 +296,8 @@ 出力の選択 The tooltip for a button for selecting all of a command's output + + 復元済み + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalControl/Resources/ko-KR/Resources.resw index 2200c028d9..40093b077a 100644 --- a/src/cascadia/TerminalControl/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/ko-KR/Resources.resw @@ -296,4 +296,8 @@ 출력 선택 The tooltip for a button for selecting all of a command's output + + 복원됨 + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/pt-BR/Resources.resw b/src/cascadia/TerminalControl/Resources/pt-BR/Resources.resw index aadafd14b1..4a5f4a42a0 100644 --- a/src/cascadia/TerminalControl/Resources/pt-BR/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/pt-BR/Resources.resw @@ -296,4 +296,8 @@ Instale a fonte ausente ou escolha outra. Selecionar saída The tooltip for a button for selecting all of a command's output + + Restaurado + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/qps-ploc/Resources.resw b/src/cascadia/TerminalControl/Resources/qps-ploc/Resources.resw index 1cd2c9e814..aa173ea721 100644 --- a/src/cascadia/TerminalControl/Resources/qps-ploc/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/qps-ploc/Resources.resw @@ -296,4 +296,8 @@ Ŝ℮ĺèčť οùτφųţ !!! The tooltip for a button for selecting all of a command's output + + Яєŝťòяёď !! + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/qps-ploca/Resources.resw b/src/cascadia/TerminalControl/Resources/qps-ploca/Resources.resw index 1cd2c9e814..aa173ea721 100644 --- a/src/cascadia/TerminalControl/Resources/qps-ploca/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/qps-ploca/Resources.resw @@ -296,4 +296,8 @@ Ŝ℮ĺèčť οùτφųţ !!! The tooltip for a button for selecting all of a command's output + + Яєŝťòяёď !! + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/qps-plocm/Resources.resw b/src/cascadia/TerminalControl/Resources/qps-plocm/Resources.resw index 1cd2c9e814..aa173ea721 100644 --- a/src/cascadia/TerminalControl/Resources/qps-plocm/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/qps-plocm/Resources.resw @@ -296,4 +296,8 @@ Ŝ℮ĺèčť οùτφųţ !!! The tooltip for a button for selecting all of a command's output + + Яєŝťòяёď !! + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/ru-RU/Resources.resw b/src/cascadia/TerminalControl/Resources/ru-RU/Resources.resw index 8b0c908a9b..96de97c8d7 100644 --- a/src/cascadia/TerminalControl/Resources/ru-RU/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/ru-RU/Resources.resw @@ -296,4 +296,8 @@ Выбрать вывод The tooltip for a button for selecting all of a command's output + + Восстановлено + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/zh-CN/Resources.resw b/src/cascadia/TerminalControl/Resources/zh-CN/Resources.resw index c331dab19a..b49a6a96cc 100644 --- a/src/cascadia/TerminalControl/Resources/zh-CN/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/zh-CN/Resources.resw @@ -296,4 +296,8 @@ 选择输出 The tooltip for a button for selecting all of a command's output + + 已还原 + "Restored" as in "This content was restored" + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/Resources/zh-TW/Resources.resw b/src/cascadia/TerminalControl/Resources/zh-TW/Resources.resw index 31d8034d2a..f6ea981f4e 100644 --- a/src/cascadia/TerminalControl/Resources/zh-TW/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/zh-TW/Resources.resw @@ -296,4 +296,8 @@ 選取輸出 The tooltip for a button for selecting all of a command's output + + 已還原 + "Restored" as in "This content was restored" + \ No newline at end of file From 8d67477a1a1b6d26669736057f4ef2f318400ac8 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 26 Apr 2024 13:20:21 +0200 Subject: [PATCH 21/53] AtlasEngine: Improve debuggability (#17136) This is some test code and natvis fixes that I used to trace down an issue during font rendering. --- src/renderer/atlas/BackendD3D.cpp | 35 ++++++++++++++++++++++++++++++- tools/ConsoleTypes.natvis | 22 ++++++++----------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index d6fb766f33..43b636336a 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -1242,7 +1242,7 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawGlyph(const RenderingPayload& p, c DWRITE_RENDERING_MODE renderingMode{}; DWRITE_GRID_FIT_MODE gridFitMode{}; THROW_IF_FAILED(fontFaceEntry.fontFace->GetRecommendedRenderingMode( - /* fontEmSize */ fontEmSize, + /* fontEmSize */ glyphRun.fontEmSize, /* dpiX */ 1, // fontEmSize is already in pixel /* dpiY */ 1, // fontEmSize is already in pixel /* transform */ nullptr, @@ -1294,6 +1294,39 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawGlyph(const RenderingPayload& p, c // The buffer now contains a grayscale alpha mask. #endif + // This code finds the local font file path. Useful for debugging as it + // gets you the font.ttf <> glyphIndex pair to uniquely identify glyphs. +#if 0 + std::vector paths; + + UINT32 numberOfFiles; + THROW_IF_FAILED(fontFaceEntry.fontFace->GetFiles(&numberOfFiles, nullptr)); + wil::com_ptr fontFiles[8]; + THROW_IF_FAILED(fontFaceEntry.fontFace->GetFiles(&numberOfFiles, fontFiles[0].addressof())); + + for (UINT32 i = 0; i < numberOfFiles; ++i) + { + wil::com_ptr loader; + THROW_IF_FAILED(fontFiles[i]->GetLoader(loader.addressof())); + + void const* fontFileReferenceKey; + UINT32 fontFileReferenceKeySize; + THROW_IF_FAILED(fontFiles[i]->GetReferenceKey(&fontFileReferenceKey, &fontFileReferenceKeySize)); + + if (const auto localLoader = loader.try_query()) + { + UINT32 filePathLength; + THROW_IF_FAILED(localLoader->GetFilePathLengthFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &filePathLength)); + + filePathLength++; + std::wstring filePath(filePathLength, L'\0'); + THROW_IF_FAILED(localLoader->GetFilePathFromKey(fontFileReferenceKey, fontFileReferenceKeySize, filePath.data(), filePathLength)); + + paths.emplace_back(std::move(filePath)); + } + } +#endif + const int scale = row.lineRendition != LineRendition::SingleWidth; D2D1_MATRIX_3X2_F transform = identityTransform; diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis index fa1975e3e4..855fcad1fb 100644 --- a/tools/ConsoleTypes.natvis +++ b/tools/ConsoleTypes.natvis @@ -124,7 +124,7 @@ - {{ size={_load / $T2} }} + {{ size={_load / $T3} }} _capacity @@ -161,22 +161,18 @@ - (empty) - {glyphIndex} - - - - {(void*)fontFace.m_ptr}, {lineRendition} - - glyphs - + (empty) + {glyphIndex} - (empty) - {*inner._Mypair._Myval2} + (empty) + {*fontFace.m_ptr} - *inner._Mypair._Myval2 + glyphs[0] + glyphs[1] + glyphs[2] + glyphs[3] From 41bb28c46d0e03d4b652cc340bb63790e0868ece Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Fri, 26 Apr 2024 10:08:37 -0500 Subject: [PATCH 22/53] Update Cascadia Code to 2404.23 (#17137) This update adds support for: - Unicode 16 Large Type Pieces (they are really cool, you *have* to see them) - Unicode 13 Sextants (U+1FB00 - U+1FB3B) - Octants, sedecimants, eights, miscellanrous blocks, separated quadrants and sextants, and diagonals - Segmented digits (think LED numbers) - Checkerboards It also fixes the coordinate system used in all of the blocks, half-blocks, quadrants and eights for consistency. This update does **not** include the new "Nerd Fonts" variant of Cascadia Code or Cascadia Mono. With big thanks to @PhMajerus for contributing all of the new symbols for legacy computing. See microsoft/cascadia-code#723, microsoft/cascadia-code#708 and microsoft/cascadia-code#727 for more details. --- res/fonts/CascadiaCode.ttf | Bin 648732 -> 738504 bytes res/fonts/CascadiaCodeItalic.ttf | Bin 443804 -> 540928 bytes res/fonts/CascadiaMono.ttf | Bin 624892 -> 715000 bytes res/fonts/CascadiaMonoItalic.ttf | Bin 427072 -> 524508 bytes res/fonts/README.md | 4 ++-- 5 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/fonts/CascadiaCode.ttf b/res/fonts/CascadiaCode.ttf index b47bf6338984d57d86ad2dd16362413d9b270aa8..4e55e88db5afc27f99adb107184c9bbfa085a8b8 100644 GIT binary patch delta 199386 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 delta 109424 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�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 diff --git a/res/fonts/CascadiaMono.ttf b/res/fonts/CascadiaMono.ttf index d15637efe089b7ff39d781e5929ed7138a65217f..4c9f3d2158f18d2cfd3b9386c1b32cb76e2a711e 100644 GIT binary patch delta 178045 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

c&22HxEajAs?Wn1OdM1LIjGFlON0&%k(A8H^ct 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 =BdZ##Is+rCCaWm}Bda;9F9Rd1KWi8RBWomU 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%0D=(H1oF$ruY7`D9E5jeIg@gGN3XCxJ#j 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 S@y={1U&#~e7XK_@U;qFDqpe~9 delta 87558 zcmexyQTxwCwR#3d1_lNRMg|5Jh6a~lPxs%<(_~r=P)?f%VJ^1_lN* z|6qNisV$~03=B*h3=9kj!6A+zuLZ7$F|eL_z`(%JmsplqU-T^h*BS=avjq$c3`)tl zi3Mg1XPX%qOk5Zkgl8p}B^G7R$dG4XJv)Vgfk7*|tVAKB(Q6q4LsSj}1EW(~L3-{Z z$$3!>4DJsY82G28l_eIno0gg}u%0~tvM@cdxPZZ$L5+dsZw&(jgFt#tW!l%8qOA-J zy&D*qw{J-=t4}Oq?mn8wz=~SL-M*^C^V@u7;0A>p1H+B;4^tT!7#Qx}O}adJ1EZP*NFhi7hI#TqB0Tkz-!XcK z9bowRn}LNv0VEmh=;g?8fPwe_-_1TucMT`Mw2>2KVqjokW?*0tVPFR7Vh~|q5pLV8 zW82EesJ3~NT_`K73IhYD*W`mHB3hhZIR7y)aR1^l;jsdl$H2g2#^cApz!Sm~#uEV& z;faHgo4s5fux~c-X=Pz#*le(+Te04Uf#FFK1H+RF28JgS7#N=NSwPFSHmKUaB)Nypm#Icy*D1;nhn9 zhS!V?46h{_7+xDPFue9;V0fL#!0@_~f#LN;28P!w85mw4WMFuGlY!y&M+SyBoD2;0 zZxk6A-dHj)ya{Arc$3M%@OBmh!`lrE3~!GyFuc9T!0`4P1H(H$28MTP3=Hq=7#QA# zF)+N#VPJUI#K7?WAp^t5N(P3H{R|8rmohMX+{?i5@hStu$JY!DpBNb!K1nh#d@^KU z_~gmJ@F|gj;nNWYhR+@h44)Gi7(Q1pFnpfCP|xsr1p~w90}KqGZ!j=?{=mTSg@b|N ziyQ;P7ZV1CFJ24`UlJG?zLYUAeCc6e__B(D;mZjIhA$5o7{2^qVE8J)!0=Urf#ItI z1H;z{28OQ%3=Cg87#O}TV_^8Yhk@biB?gACFBllU{$pVH7RJEv?I{Dpx4#Sw--Q_% zzH2iu{7|fCVEF08!0R-yzdtfC{MpFB@Yk4u;qN;JhJS1f4F6;p82*_sF#Pjk zVEC8B!0@kvf#Kf-28Msj7#RNTV_^7qje+6cI|hdTEDQ|)>tz@i{+lo`{P$sC_@BhU z@V|2D7�X7&!tN7&!_V7&#^~ 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 D2+^lb diff --git a/res/fonts/CascadiaMonoItalic.ttf b/res/fonts/CascadiaMonoItalic.ttf index 2fba9215ffcb47a637298e3e56de49f37b94be16..c13eddbb6fabf2faec737a2a9b2d98d1785d06f0 100644 GIT binary patch delta 115072 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 delta 19663 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 diff --git a/res/fonts/README.md b/res/fonts/README.md index bb739fee85..e30d23aa8b 100644 --- a/res/fonts/README.md +++ b/res/fonts/README.md @@ -8,5 +8,5 @@ Please consult the [license](https://raw.githubusercontent.com/microsoft/cascadi ### Fonts Included -* Cascadia Code, Cascadia Mono (2111.01) - * from microsoft/cascadia-code@de36d62e777d34d3bed92a7e23988e5d61e0ba02 +* Cascadia Code, Cascadia Mono (2404.23) + * from microsoft/cascadia-code@1034791e5fc6e060a448d2b29cd94a6c683edb36 From d14ff939dc418fa04401304fdef539424dbb5bd5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 26 Apr 2024 14:23:39 -0700 Subject: [PATCH 23/53] Fix repositioning with the cursor, again (#17141) This shouldn't have ever worked...? This looks like it was a typo and should have been `mark.end`. Thanks @joadoumie for asking about the moving the cursor in the prompt, that convo lead to me finding this. --- src/cascadia/TerminalControl/ControlCore.cpp | 157 ++++++++++--------- src/cascadia/TerminalControl/ControlCore.h | 1 + 2 files changed, 82 insertions(+), 76 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index da46f21b65..468faeba54 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -2009,93 +2009,98 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (_settings->RepositionCursorWithMouse()) // This is also mode==Char && !shiftEnabled { - // If we're handling a single left click, without shift pressed, and - // outside mouse mode, AND the user has RepositionCursorWithMouse turned - // on, let's try to move the cursor. - // - // We'll only move the cursor if the user has clicked after the last - // mark, if there is one. That means the user also needs to set up - // shell integration to enable this feature. - // - // As noted in GH #8573, there's plenty of edge cases with this - // approach, but it's good enough to bring value to 90% of use cases. - const auto cursorPos{ _terminal->GetCursorPosition() }; + _repositionCursorWithMouse(terminalPosition); + } + _updateSelectionUI(); + } - // Does the current buffer line have a mark on it? - const auto& marks{ _terminal->GetMarkExtents() }; - if (!marks.empty()) + void ControlCore::_repositionCursorWithMouse(const til::point terminalPosition) + { + // If we're handling a single left click, without shift pressed, and + // outside mouse mode, AND the user has RepositionCursorWithMouse turned + // on, let's try to move the cursor. + // + // We'll only move the cursor if the user has clicked after the last + // mark, if there is one. That means the user also needs to set up + // shell integration to enable this feature. + // + // As noted in GH #8573, there's plenty of edge cases with this + // approach, but it's good enough to bring value to 90% of use cases. + const auto cursorPos{ _terminal->GetCursorPosition() }; + + // Does the current buffer line have a mark on it? + const auto& marks{ _terminal->GetMarkExtents() }; + if (!marks.empty()) + { + const auto& last{ marks.back() }; + const auto [start, end] = last.GetExtent(); + const auto bufferSize = _terminal->GetTextBuffer().GetSize(); + auto lastNonSpace = _terminal->GetTextBuffer().GetLastNonSpaceCharacter(); + bufferSize.IncrementInBounds(lastNonSpace, true); + + // If the user clicked off to the right side of the prompt, we + // want to send keystrokes to the last character in the prompt +1. + // + // We don't want to send too many here. In CMD, if the user's + // last command is longer than what they've currently typed, and + // they press right arrow at the end of the prompt, COOKED_READ + // will fill in characters from the previous command. + // + // By only sending keypresses to the end of the command + 1, we + // should leave the cursor at the very end of the prompt, + // without adding any characters from a previous command. + + // terminalPosition is viewport-relative. + const auto bufferPos = _terminal->GetViewport().Origin() + terminalPosition; + if (bufferPos.y > lastNonSpace.y) { - const auto& last{ marks.back() }; - const auto [start, end] = last.GetExtent(); - const auto bufferSize = _terminal->GetTextBuffer().GetSize(); - auto lastNonSpace = _terminal->GetTextBuffer().GetLastNonSpaceCharacter(); - bufferSize.IncrementInBounds(lastNonSpace, true); + // Clicked under the prompt. Bail. + return; + } - // If the user clicked off to the right side of the prompt, we - // want to send keystrokes to the last character in the prompt +1. - // - // We don't want to send too many here. In CMD, if the user's - // last command is longer than what they've currently typed, and - // they press right arrow at the end of the prompt, COOKED_READ - // will fill in characters from the previous command. - // - // By only sending keypresses to the end of the command + 1, we - // should leave the cursor at the very end of the prompt, - // without adding any characters from a previous command. + // Limit the click to 1 past the last character on the last line. + const auto clampedClick = std::min(bufferPos, lastNonSpace); - // terminalPosition is viewport-relative. - const auto bufferPos = _terminal->GetViewport().Origin() + terminalPosition; - if (bufferPos.y > lastNonSpace.y) + if (clampedClick >= last.end) + { + // Get the distance between the cursor and the click, in cells. + + // First, make sure to iterate from the first point to the + // second. The user may have clicked _earlier_ in the + // buffer! + auto goRight = clampedClick > cursorPos; + const auto startPoint = goRight ? cursorPos : clampedClick; + const auto endPoint = goRight ? clampedClick : cursorPos; + + const auto delta = _terminal->GetTextBuffer().GetCellDistance(startPoint, endPoint); + const WORD key = goRight ? VK_RIGHT : VK_LEFT; + + std::wstring buffer; + const auto append = [&](TerminalInput::OutputType&& out) { + if (out) + { + buffer.append(std::move(*out)); + } + }; + + // Send an up and a down once per cell. This won't + // accurately handle wide characters, or continuation + // prompts, or cases where a single escape character in the + // command (e.g. ^[) takes up two cells. + for (size_t i = 0u; i < delta; i++) { - // Clicked under the prompt. Bail. - return; + append(_terminal->SendKeyEvent(key, 0, {}, true)); + append(_terminal->SendKeyEvent(key, 0, {}, false)); } - // Limit the click to 1 past the last character on the last line. - const auto clampedClick = std::min(bufferPos, lastNonSpace); - - if (clampedClick >= end) { - // Get the distance between the cursor and the click, in cells. - - // First, make sure to iterate from the first point to the - // second. The user may have clicked _earlier_ in the - // buffer! - auto goRight = clampedClick > cursorPos; - const auto startPoint = goRight ? cursorPos : clampedClick; - const auto endPoint = goRight ? clampedClick : cursorPos; - - const auto delta = _terminal->GetTextBuffer().GetCellDistance(startPoint, endPoint); - const WORD key = goRight ? VK_RIGHT : VK_LEFT; - - std::wstring buffer; - const auto append = [&](TerminalInput::OutputType&& out) { - if (out) - { - buffer.append(std::move(*out)); - } - }; - - // Send an up and a down once per cell. This won't - // accurately handle wide characters, or continuation - // prompts, or cases where a single escape character in the - // command (e.g. ^[) takes up two cells. - for (size_t i = 0u; i < delta; i++) - { - append(_terminal->SendKeyEvent(key, 0, {}, true)); - append(_terminal->SendKeyEvent(key, 0, {}, false)); - } - - { - // Sending input requires that we're unlocked, because - // writing the input pipe may block indefinitely. - const auto suspension = _terminal->SuspendLock(); - _sendInputToConnection(buffer); - } + // Sending input requires that we're unlocked, because + // writing the input pipe may block indefinitely. + const auto suspension = _terminal->SuspendLock(); + _sendInputToConnection(buffer); } } } - _updateSelectionUI(); } // Method Description: diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index adcaf82102..76b5d8ed95 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -403,6 +403,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _focusChanged(bool focused); void _selectSpan(til::point_span s); + void _repositionCursorWithMouse(const til::point terminalPosition); void _contextMenuSelectMark( const til::point& pos, From 378b6594bd94cf3b27f4e309a11efe25d83de0d9 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Fri, 26 Apr 2024 16:34:01 -0700 Subject: [PATCH 24/53] Add action ID to schema (#17146) Closes #17122 --- doc/cascadia/profiles.schema.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 9593ab9f6a..34945728c0 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -2200,6 +2200,10 @@ } } }, + "id": { + "description": "The ID of this command. If one isn't provided, we will generate one internally.", + "type": "string" + }, "iterateOn": { "type": "string", "description": "Used to create iterable commands based on other objects in your settings. Possible values:\n- \"profiles\" \n- \"schemes\"", From ef318a14502ee933e4034d7b4bf5739d7d394ad1 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 29 Apr 2024 12:55:09 +0100 Subject: [PATCH 25/53] Fix the DECTCEM reset position in the conpty stream (#17148) ## Summary of the Pull Request When the conpty renderer determines that it needs to hide the cursor, it does so by inserting a `DECTCEM` reset sequence at the start of the output buffer, assuming that is the start of the frame. But when the `_noFlushOnEnd` flag is set, you can have multiple frames pending in the buffer, and the `DECTCEM` sequence will then end up in the wrong place. This PR fixes the issue by saving the buffer size at the start of the frame, and using that saved offset as the insert position for the `DECTCEM` sequence. ## Validation Steps Performed I have a game that was frequently affected by this issue (the cursor would be visible when it was meant to be hidden). With this PR applied, it now works perfectly. ## PR Checklist - [x] Closes #15449 --- src/renderer/vt/XtermEngine.cpp | 3 ++- src/renderer/vt/state.cpp | 1 + src/renderer/vt/vtrenderer.hpp | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 1482235158..75fc3510fa 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -45,6 +45,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // visible, then PaintCursor will be called, and we'll set this to true // during the frame. _nextCursorIsVisible = false; + _startOfFrameBufferIndex = _buffer.size(); // Do not perform synchronization clearing in passthrough mode. // In passthrough, the terminal leads and we follow what it is @@ -112,7 +113,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // by prepending a cursor off. if (_lastCursorIsVisible != Tribool::False) { - _buffer.insert(0, "\x1b[?25l"); + _buffer.insert(_startOfFrameBufferIndex, "\x1b[?25l"); _lastCursorIsVisible = Tribool::False; } // If the cursor was NOT previously visible, then that's fine! we don't diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index ea176e42f3..881fb0c2cc 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -158,6 +158,7 @@ void VtEngine::_flushImpl() noexcept { const auto fSuccess = WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast(_buffer.size()), nullptr, nullptr); _buffer.clear(); + _startOfFrameBufferIndex = 0; if (!fSuccess) { LOG_LAST_ERROR(); diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 0135e79116..0125fe1736 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -93,6 +93,7 @@ namespace Microsoft::Console::Render protected: wil::unique_hfile _hFile; std::string _buffer; + size_t _startOfFrameBufferIndex = 0; std::string _formatBuffer; std::string _conversionBuffer; From be5a240ec633babf15f1b878a5efb4a544705ca1 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Mon, 29 Apr 2024 07:57:47 -0700 Subject: [PATCH 26/53] No longer serialize generated IDs (#17145) We will no longer serialize IDs that we generated for the user. This change is being made so that we can release user action IDs at the same time as the features that require them! Validation: Generated IDs do not get written to the json, user-made IDs still do Closes #17109 --- .../CascadiaSettingsSerialization.cpp | 2 +- .../TerminalSettingsModel/Command.cpp | 3 +- src/cascadia/TerminalSettingsModel/Command.h | 1 + .../SerializationTests.cpp | 46 ++++++------------- 4 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 37112fe76b..1deaa9eb50 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -506,7 +506,7 @@ bool SettingsLoader::FixupUserSettings() // we need to generate an ID for a command in the user settings if it doesn't already have one auto actionMap{ winrt::get_self(userSettings.globals->ActionMap()) }; - fixedUp = actionMap->GenerateIDsForActions() || fixedUp; + actionMap->GenerateIDsForActions(); return fixedUp; } diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index c20be960a6..a32ed4efa7 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -129,6 +129,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation if (const auto generatedID = actionAndArgsImpl->GenerateID(); !generatedID.empty()) { _ID = generatedID; + _IDWasGenerated = true; return true; } } @@ -445,7 +446,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value cmdJson{ Json::ValueType::objectValue }; JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); - if (!_ID.empty()) + if (!_ID.empty() && !_IDWasGenerated) { JsonUtils::SetValueForKey(cmdJson, IDKey, _ID); } diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index f22e35348c..a6821f55f3 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -88,6 +88,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::vector _keyMappings; std::optional _name; std::wstring _ID; + bool _IDWasGenerated{ false }; std::optional _iconPath; bool _nestedCommand{ false }; diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index 8d9c55b6f8..82f9f924de 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -972,36 +972,17 @@ namespace SettingsModelUnitTests ] })" }; - // Key differences: - the sendInput action now has a generated ID - // - this generated ID was created at the time of writing this test, - // and should remain robust (i.e. every time we hash the args we should get the same result) - static constexpr std::string_view newSettingsJson{ R"( - { - "actions": [ - { - "name": "foo", - "command": { "action": "sendInput", "input": "just some input" }, - "keys": "ctrl+shift+w", - "id" : "User.sendInput.)" SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH R"(" - } - ] - })" }; + implementation::SettingsLoader loader{ oldSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; + loader.MergeInboxIntoUserSettings(); + loader.FinalizeLayering(); + loader.FixupUserSettings(); + const auto settings = winrt::make_self(std::move(loader)); + const auto oldResult{ settings->ToJson() }; + const auto sendInputCmd = settings->ActionMap().GetActionByKeyChord(KeyChord{ true, false, true, false, 87, 0 }); - implementation::SettingsLoader oldLoader{ oldSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; - oldLoader.MergeInboxIntoUserSettings(); - oldLoader.FinalizeLayering(); - VERIFY_IS_TRUE(oldLoader.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); - const auto oldSettings = winrt::make_self(std::move(oldLoader)); - const auto oldResult{ oldSettings->ToJson() }; + std::string_view expectedID{ R"(User.sendInput.)" SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH }; - implementation::SettingsLoader newLoader{ newSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; - newLoader.MergeInboxIntoUserSettings(); - newLoader.FinalizeLayering(); - newLoader.FixupUserSettings(); - const auto newSettings = winrt::make_self(std::move(newLoader)); - const auto newResult{ newSettings->ToJson() }; - - VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); + VERIFY_ARE_EQUAL(sendInputCmd.ID(), winrt::to_hstring(expectedID)); } void SerializationTests::NoGeneratedIDsForIterableAndNestedCommands() @@ -1068,17 +1049,20 @@ namespace SettingsModelUnitTests implementation::SettingsLoader loader1{ settingsJson1, implementation::LoadStringResource(IDR_DEFAULTS) }; loader1.MergeInboxIntoUserSettings(); loader1.FinalizeLayering(); - VERIFY_IS_TRUE(loader1.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + loader1.FixupUserSettings(); const auto settings1 = winrt::make_self(std::move(loader1)); const auto result1{ settings1->ToJson() }; implementation::SettingsLoader loader2{ settingsJson2, implementation::LoadStringResource(IDR_DEFAULTS) }; loader2.MergeInboxIntoUserSettings(); loader2.FinalizeLayering(); - VERIFY_IS_TRUE(loader2.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + loader2.FixupUserSettings(); const auto settings2 = winrt::make_self(std::move(loader2)); const auto result2{ settings2->ToJson() }; - VERIFY_ARE_EQUAL(toString(result1), toString(result2)); + const auto sendInputCmd1 = settings1->ActionMap().GetActionByKeyChord(KeyChord{ true, false, true, false, 87, 0 }); + const auto sendInputCmd2 = settings2->ActionMap().GetActionByKeyChord(KeyChord{ true, false, true, false, 87, 0 }); + + VERIFY_ARE_EQUAL(sendInputCmd1.ID(), sendInputCmd1.ID()); } } From 5b8eadb2ea16206bac2bb1c0b06f35bd4d0aae15 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 29 Apr 2024 18:43:47 +0200 Subject: [PATCH 27/53] Make UTextFromTextBuffer newline aware (#17120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR achieves two things: * When encountering rows with newlines (`WasForceWrapped` = `false`) we'll now copy the contents out of the row and append a `\n`. To make `utext_clone` cheap, it adds a reference counted buffer. * Text extraction in `Terminal::GetHyperlinkAtBufferPosition` was fixed by using a higher level `TextBuffer::GetPlainText` instead of iterating through each cell. Closes #16676 Closes #17065 ## Validation Steps Performed * In pwsh execute the following: ``"`e[999C`e[22Dhttps://example.com/foo`nbar"`` * Hovering over the URL only underlines `.../foo` and not `bar` ✅ * The tooltip ends in `.../foo` and not `.../fo` ✅ --- src/buffer/out/UTextAdapter.cpp | 135 +++++++++++++++++++++++-- src/buffer/out/UTextAdapter.h | 3 +- src/buffer/out/textBuffer.cpp | 21 +--- src/buffer/out/textBuffer.hpp | 2 +- src/cascadia/TerminalCore/Terminal.cpp | 9 +- 5 files changed, 131 insertions(+), 39 deletions(-) diff --git a/src/buffer/out/UTextAdapter.cpp b/src/buffer/out/UTextAdapter.cpp index 7dec8d2dc0..84ba9583f0 100644 --- a/src/buffer/out/UTextAdapter.cpp +++ b/src/buffer/out/UTextAdapter.cpp @@ -6,19 +6,87 @@ #include "textBuffer.hpp" +// All of these are somewhat annoying when trying to implement RefcountBuffer. +// You can't stuff a unique_ptr into ut->q (= void*) after all. +#pragma warning(disable : 26402) // Return a scoped object instead of a heap-allocated if it has a move constructor (r.3). +#pragma warning(disable : 26403) // Reset or explicitly delete an owner pointer '...' (r.3). +#pragma warning(disable : 26409) // Avoid calling new and delete explicitly, use std::make_unique instead (r.11). + struct RowRange { til::CoordType begin; til::CoordType end; }; +struct RefcountBuffer +{ + size_t references; + size_t capacity; + wchar_t data[1]; + + static RefcountBuffer* EnsureCapacityForOverwrite(RefcountBuffer* buffer, size_t capacity) + { + // We must not just ensure that `buffer` has at least `capacity`, but also that its reference count is <= 1, because otherwise we would resize a shared buffer. + if (buffer != nullptr && buffer->references <= 1 && buffer->capacity >= capacity) + { + return buffer; + } + + const auto oldCapacity = buffer ? buffer->capacity << 1 : 0; + const auto newCapacity = std::max(capacity + 128, oldCapacity); + const auto newBuffer = static_cast(::operator new(sizeof(RefcountBuffer) - sizeof(data) + newCapacity * sizeof(wchar_t))); + + if (!newBuffer) + { + return nullptr; + } + + if (buffer) + { + buffer->Release(); + } + + // Copying the old buffer's data is not necessary because utextAccess() will scribble right over it. + newBuffer->references = 1; + newBuffer->capacity = newCapacity; + return newBuffer; + } + + void AddRef() noexcept + { + // With our usage patterns, either of these two would indicate + // an unbalanced AddRef/Release or a memory corruption. + assert(references > 0 && references < 1000); + references++; + } + + void Release() noexcept + { + // With our usage patterns, either of these two would indicate + // an unbalanced AddRef/Release or a memory corruption. + assert(references > 0 && references < 1000); + if (--references == 0) + { + ::operator delete(this); + } + } +}; + constexpr size_t& accessLength(UText* ut) noexcept { + static_assert(sizeof(ut->p) == sizeof(size_t)); return *std::bit_cast(&ut->p); } +constexpr RefcountBuffer*& accessBuffer(UText* ut) noexcept +{ + static_assert(sizeof(ut->q) == sizeof(RefcountBuffer*)); + return *std::bit_cast(&ut->q); +} + constexpr RowRange& accessRowRange(UText* ut) noexcept { + static_assert(sizeof(ut->a) == sizeof(RowRange)); return *std::bit_cast(&ut->a); } @@ -56,11 +124,16 @@ static UText* U_CALLCONV utextClone(UText* dest, const UText* src, UBool deep, U } dest = utext_setup(dest, 0, status); - if (*status <= U_ZERO_ERROR) + if (*status > U_ZERO_ERROR) { - memcpy(dest, src, sizeof(UText)); + return dest; } + memcpy(dest, src, sizeof(UText)); + if (const auto buf = accessBuffer(dest)) + { + buf->AddRef(); + } return dest; } @@ -82,7 +155,9 @@ try for (til::CoordType y = range.begin; y < range.end; ++y) { - length += textBuffer.GetRowByOffset(y).GetText().size(); + const auto& row = textBuffer.GetRowByOffset(y); + // Later down below we'll add a newline to the text if !wasWrapForced, so we need to account for that here. + length += row.GetText().size() + !row.WasWrapForced(); } accessLength(ut) = length; @@ -126,11 +201,13 @@ try const auto range = accessRowRange(ut); auto start = ut->chunkNativeStart; auto limit = ut->chunkNativeLimit; - auto y = accessCurrentRow(ut); - std::wstring_view text; if (neededIndex < start || neededIndex >= limit) { + auto y = accessCurrentRow(ut); + std::wstring_view text; + bool wasWrapForced = false; + if (neededIndex < start) { do @@ -138,12 +215,17 @@ try --y; if (y < range.begin) { + assert(false); return false; } - text = textBuffer.GetRowByOffset(y).GetText(); + const auto& row = textBuffer.GetRowByOffset(y); + text = row.GetText(); + wasWrapForced = row.WasWrapForced(); + limit = start; - start -= text.size(); + // Later down below we'll add a newline to the text if !wasWrapForced, so we need to account for that here. + start -= text.size() + !wasWrapForced; } while (neededIndex < start); } else @@ -153,15 +235,32 @@ try ++y; if (y >= range.end) { + assert(false); return false; } - text = textBuffer.GetRowByOffset(y).GetText(); + const auto& row = textBuffer.GetRowByOffset(y); + text = row.GetText(); + wasWrapForced = row.WasWrapForced(); + start = limit; - limit += text.size(); + // Later down below we'll add a newline to the text if !wasWrapForced, so we need to account for that here. + limit += text.size() + !wasWrapForced; } while (neededIndex >= limit); } + if (!wasWrapForced) + { + const auto newSize = text.size() + 1; + const auto buffer = RefcountBuffer::EnsureCapacityForOverwrite(accessBuffer(ut), newSize); + + memcpy(&buffer->data[0], text.data(), text.size() * sizeof(wchar_t)); + til::at(buffer->data, text.size()) = L'\n'; + + text = { &buffer->data[0], newSize }; + accessBuffer(ut) = buffer; + } + accessCurrentRow(ut) = y; ut->chunkNativeStart = start; ut->chunkNativeLimit = limit; @@ -256,18 +355,32 @@ catch (...) return 0; } +static void U_CALLCONV utextClose(UText* ut) noexcept +{ + if (const auto buffer = accessBuffer(ut)) + { + buffer->Release(); + } +} + static constexpr UTextFuncs utextFuncs{ .tableSize = sizeof(UTextFuncs), .clone = utextClone, .nativeLength = utextNativeLength, .access = utextAccess, + .close = utextClose, }; // Creates a UText from the given TextBuffer that spans rows [rowBeg,RowEnd). -UText Microsoft::Console::ICU::UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept +Microsoft::Console::ICU::unique_utext Microsoft::Console::ICU::UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept { #pragma warning(suppress : 26477) // Use 'nullptr' rather than 0 or NULL (es.47). - UText ut = UTEXT_INITIALIZER; + unique_utext ut{ UTEXT_INITIALIZER }; + + UErrorCode status = U_ZERO_ERROR; + utext_setup(&ut, 0, &status); + FAIL_FAST_IF(status > U_ZERO_ERROR); + ut.providerProperties = (1 << UTEXT_PROVIDER_LENGTH_IS_EXPENSIVE) | (1 << UTEXT_PROVIDER_STABLE_CHUNKS); ut.pFuncs = &utextFuncs; ut.context = &textBuffer; diff --git a/src/buffer/out/UTextAdapter.h b/src/buffer/out/UTextAdapter.h index c8c325143e..39903627b5 100644 --- a/src/buffer/out/UTextAdapter.h +++ b/src/buffer/out/UTextAdapter.h @@ -10,8 +10,9 @@ class TextBuffer; namespace Microsoft::Console::ICU { using unique_uregex = wistd::unique_ptr>; + using unique_utext = wil::unique_struct; - UText UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept; + unique_utext UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept; unique_uregex CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept; til::point_span BufferRangeFromMatch(UText* ut, URegularExpression* re); } diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 1b5b28c621..10c70646e9 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1995,25 +1995,10 @@ size_t TextBuffer::SpanLength(const til::point coordStart, const til::point coor // - end - where to end getting text // Return Value: // - Just the text. -std::wstring TextBuffer::GetPlainText(const til::point& start, const til::point& end) const +std::wstring TextBuffer::GetPlainText(const til::point start, const til::point end) const { - std::wstring text; - auto spanLength = SpanLength(start, end); - text.reserve(spanLength); - - auto it = GetCellDataAt(start); - - for (; it && spanLength > 0; ++it, --spanLength) - { - const auto& cell = *it; - if (cell.DbcsAttr() != DbcsAttribute::Trailing) - { - const auto chars = cell.Chars(); - text.append(chars); - } - } - - return text; + const auto req = CopyRequest::FromConfig(*this, start, end, true, false, false, false); + return GetPlainText(req); } // Routine Description: diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index f250749c57..5a8a5c1654 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -196,7 +196,7 @@ public: size_t SpanLength(const til::point coordStart, const til::point coordEnd) const; - std::wstring GetPlainText(const til::point& start, const til::point& end) const; + std::wstring GetPlainText(til::point start, til::point end) const; struct CopyRequest { diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 854d54394d..80f89a0632 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -522,14 +522,7 @@ std::wstring Terminal::GetHyperlinkAtBufferPosition(const til::point bufferPos) // Case 2 - Step 2: get the auto-detected hyperlink if (result.has_value() && result->value == _hyperlinkPatternId) { - std::wstring uri; - const auto startIter = _activeBuffer().GetCellDataAt(result->start); - const auto endIter = _activeBuffer().GetCellDataAt(result->stop); - for (auto iter = startIter; iter != endIter; ++iter) - { - uri += iter->Chars(); - } - return uri; + return _activeBuffer().GetPlainText(result->start, result->stop); } return {}; } From 5d2d3856a76d0cd3acb80cc06adedfce85349c65 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 29 Apr 2024 12:52:00 -0500 Subject: [PATCH 28/53] build: force the latest VCToolsVersion; see DD-1541167 for more (#17156) Stolen from PowerToys. BODGY --- build/pipelines/templates-v2/job-build-project.yml | 4 ++++ build/scripts/Set-LatestVCToolsVersion.ps1 | 8 ++++++++ src/interactivity/onecore/SystemConfigurationProvider.cpp | 1 + 3 files changed, 13 insertions(+) create mode 100644 build/scripts/Set-LatestVCToolsVersion.ps1 diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml index 35d342236f..d3c10bb822 100644 --- a/build/pipelines/templates-v2/job-build-project.yml +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -142,6 +142,10 @@ jobs: - template: .\steps-restore-nuget.yml + - pwsh: |- + .\build\scripts\Set-LatestVCToolsVersion.ps1 + displayName: Work around DD-1541167 (VCToolsVersion) + - ${{ parameters.beforeBuildSteps }} - task: VSBuild@1 diff --git a/build/scripts/Set-LatestVCToolsVersion.ps1 b/build/scripts/Set-LatestVCToolsVersion.ps1 new file mode 100644 index 0000000000..54a43b3a01 --- /dev/null +++ b/build/scripts/Set-LatestVCToolsVersion.ps1 @@ -0,0 +1,8 @@ +$VSInstances = ([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -include packages -format xml)) +$VSPackages = $VSInstances.instances.instance.packages.package +$LatestVCPackage = ($VSInstances.instances.instance.packages.package | ? { $_.id -eq "Microsoft.VisualCpp.CRT.Source" }) +$LatestVCToolsVersion = $LatestVCPackage.version; + +Write-Output "Latest VCToolsVersion: $LatestVCToolsVersion" +Write-Output "Updating VCToolsVersion environment variable for job" +Write-Output "##vso[task.setvariable variable=VCToolsVersion]$LatestVCToolsVersion" diff --git a/src/interactivity/onecore/SystemConfigurationProvider.cpp b/src/interactivity/onecore/SystemConfigurationProvider.cpp index 21de54447a..7e5b7c59ab 100644 --- a/src/interactivity/onecore/SystemConfigurationProvider.cpp +++ b/src/interactivity/onecore/SystemConfigurationProvider.cpp @@ -61,6 +61,7 @@ void SystemConfigurationProvider::GetSettingsFromLink( // Hence, we make it seem like the console is in fact configured to use a // TrueType font by the user. +#pragma warning(suppress : 26485) // This isn't even really _supposed to be_ an array-to-pointer decay: it's passed as a string view. pLinkSettings->SetFaceName(DEFAULT_TT_FONT_FACENAME); pLinkSettings->SetFontFamily(TMPF_TRUETYPE); From af91e6ef582cc8303ef712d656afb27eb062061b Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 29 Apr 2024 12:58:55 -0500 Subject: [PATCH 29/53] [clang-tidy] Remove in-product uses of std::bind (#16870) These changes were automatically generated by clang-tidy. ``` clang-tidy --checks=modernize-avoid-bind --fix ``` I have not bothered with the test code. --------- Co-authored-by: Mike Griese --- src/cascadia/TerminalControl/ControlCore.cpp | 14 +++++++------- src/cascadia/WindowsTerminal/AppHost.cpp | 10 ++-------- src/host/VtInputThread.cpp | 4 ++-- src/host/srvinit.cpp | 2 +- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 468faeba54..7e2d13ee7d 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -86,25 +86,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation // GH#8969: pre-seed working directory to prevent potential races _terminal->SetWorkingDirectory(_settings->StartingDirectory()); - auto pfnCopyToClipboard = std::bind(&ControlCore::_terminalCopyToClipboard, this, std::placeholders::_1); + auto pfnCopyToClipboard = [this](auto&& PH1) { _terminalCopyToClipboard(std::forward(PH1)); }; _terminal->SetCopyToClipboardCallback(pfnCopyToClipboard); - auto pfnWarningBell = std::bind(&ControlCore::_terminalWarningBell, this); + auto pfnWarningBell = [this] { _terminalWarningBell(); }; _terminal->SetWarningBellCallback(pfnWarningBell); - auto pfnTitleChanged = std::bind(&ControlCore::_terminalTitleChanged, this, std::placeholders::_1); + auto pfnTitleChanged = [this](auto&& PH1) { _terminalTitleChanged(std::forward(PH1)); }; _terminal->SetTitleChangedCallback(pfnTitleChanged); - auto pfnScrollPositionChanged = std::bind(&ControlCore::_terminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + auto pfnScrollPositionChanged = [this](auto&& PH1, auto&& PH2, auto&& PH3) { _terminalScrollPositionChanged(std::forward(PH1), std::forward(PH2), std::forward(PH3)); }; _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged); - auto pfnTerminalTaskbarProgressChanged = std::bind(&ControlCore::_terminalTaskbarProgressChanged, this); + auto pfnTerminalTaskbarProgressChanged = [this] { _terminalTaskbarProgressChanged(); }; _terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged); - auto pfnShowWindowChanged = std::bind(&ControlCore::_terminalShowWindowChanged, this, std::placeholders::_1); + auto pfnShowWindowChanged = [this](auto&& PH1) { _terminalShowWindowChanged(std::forward(PH1)); }; _terminal->SetShowWindowCallback(pfnShowWindowChanged); - auto pfnPlayMidiNote = std::bind(&ControlCore::_terminalPlayMidiNote, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + auto pfnPlayMidiNote = [this](auto&& PH1, auto&& PH2, auto&& PH3) { _terminalPlayMidiNote(std::forward(PH1), std::forward(PH2), std::forward(PH3)); }; _terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote); auto pfnCompletionsChanged = [=](auto&& menuJson, auto&& replaceLength) { _terminalCompletionsChanged(menuJson, replaceLength); }; diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 5470db596f..94585f4391 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -73,10 +73,7 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, _window->SetMinimizeToNotificationAreaBehavior(_windowLogic.GetMinimizeToNotificationArea()); // Tell the window to callback to us when it's about to handle a WM_CREATE - auto pfn = std::bind(&AppHost::_HandleCreateWindow, - this, - std::placeholders::_1, - std::placeholders::_2); + auto pfn = [this](auto&& PH1, auto&& PH2) { _HandleCreateWindow(std::forward(PH1), std::forward(PH2)); }; _window->SetCreateCallback(pfn); _windowCallbacks.MouseScrolled = _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); @@ -394,10 +391,7 @@ void AppHost::Initialize() // while the screen is off. TerminalTrySetAutoCompleteAnimationsWhenOccluded(static_cast<::IUnknown*>(winrt::get_abi(_windowLogic.GetRoot())), true); - _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::TerminalWindow::CalcSnappedDimension, - _windowLogic, - std::placeholders::_1, - std::placeholders::_2)); + _window->SetSnapDimensionCallback([this](auto&& PH1, auto&& PH2) { return _windowLogic.CalcSnappedDimension(std::forward(PH1), std::forward(PH2)); }); // Create a throttled function for updating the window state, to match the // one requested by the pty. A 200ms delay was chosen because it's the diff --git a/src/host/VtInputThread.cpp b/src/host/VtInputThread.cpp index b9b2aefb0a..9b2f435e19 100644 --- a/src/host/VtInputThread.cpp +++ b/src/host/VtInputThread.cpp @@ -42,11 +42,11 @@ VtInputThread::VtInputThread(_In_ wil::unique_hfile hPipe, _pInputStateMachine = std::make_unique(std::move(engine)); // we need this callback to be able to flush an unknown input sequence to the app - auto flushCallback = std::bind(&StateMachine::FlushToTerminal, _pInputStateMachine.get()); + auto flushCallback = [capture0 = _pInputStateMachine.get()] { return capture0->FlushToTerminal(); }; engineRef->SetFlushToInputQueueCallback(flushCallback); // we need this callback to capture the reply if someone requests a status from the terminal - _pfnSetLookingForDSR = std::bind(&InputStateMachineEngine::SetLookingForDSR, engineRef, std::placeholders::_1); + _pfnSetLookingForDSR = [engineRef](auto&& PH1) { engineRef->SetLookingForDSR(std::forward(PH1)); }; } // Function Description: diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp index 3fda2556ca..9f7d7adc78 100644 --- a/src/host/srvinit.cpp +++ b/src/host/srvinit.cpp @@ -882,7 +882,7 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, // Set up the renderer to be used to calculate the width of a glyph, // should we be unable to figure out its width another way. - auto pfn = std::bind(&Renderer::IsGlyphWideByFont, static_cast(g.pRender), std::placeholders::_1); + auto pfn = [ObjectPtr = static_cast(g.pRender)](auto&& PH1) { return ObjectPtr->IsGlyphWideByFont(std::forward(PH1)); }; SetGlyphWidthFallback(pfn); } catch (...) From 6bc7b9e68b9378b056d3fe0ace615226b0fb6c25 Mon Sep 17 00:00:00 2001 From: Yusuf Al-Khawaldeh <114036712+uoRetr0@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:59:59 -0400 Subject: [PATCH 30/53] Fix the bad default light theme selection background color (#16789) ## Summary of the Pull Request Fixed default selection background colors with light schemes. Default color now matches the scheme and contrasts well ## References and Relevant Issues none ## Detailed Description of the Pull Request / Additional comments This is my first contribution ever :) Even though its simple, im happy to help ## Validation Steps Performed ## PR Checklist - [ ] Closes #8716 - [ ] Tests added/passed - [ ] Documentation updated - If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx - [ ] Schema updated (if necessary) --- src/cascadia/TerminalSettingsModel/defaults.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 2c79c63b7c..468c2120ac 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -174,7 +174,7 @@ "foreground": "#383A42", "background": "#FAFAFA", "cursorColor": "#4F525D", - "selectionBackground": "#4F525D", + "selectionBackground": "#383A42", "black": "#383A42", "red": "#E45649", "green": "#50A14F", @@ -219,7 +219,7 @@ "foreground": "#657B83", "background": "#FDF6E3", "cursorColor": "#002B36", - "selectionBackground": "#073642", + "selectionBackground": "#2C4D57", "black": "#002B36", "red": "#DC322F", "green": "#859900", @@ -264,7 +264,7 @@ "foreground": "#555753", "background": "#FFFFFF", "cursorColor": "#000000", - "selectionBackground": "#555753", + "selectionBackground": "#141414", "black": "#000000", "red": "#CC0000", "green": "#4E9A06", From 32fbb16d43ddfcdd039a61c5af051a16b972faeb Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 30 Apr 2024 22:48:05 +0200 Subject: [PATCH 31/53] Fix search constantly triggering a scroll (#17132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This addresses a review comment left by tusharsnx in #17092 which I forgot to fix before merging the PR. The fix itself is somewhat simple: `Terminal::SetSearchHighlightFocused` triggers a scroll if the target is outside of the current (scrolled) viewport and avoiding the call unless necessary fixes it. To do it properly though, I've split up `Search::ResetIfStale` into `IsStale` and `Reset`. Now we can properly detect staleness in advance and branch out the search reset cleanly. Additionally, I've taken the liberty to replace the `IVector` in `SearchResultRows` with a direct `const std::vector&` into `Searcher`. This removes a bunch of code and makes it faster to boot. ## Validation Steps Performed * Print lots of text * Search a common letter * Scroll up * Doesn't scroll back down ✅ * Hold enter to search more occurrences scrolls up as needed ✅ * `showMarksOnScrollbar` still works ✅ --- src/buffer/out/search.cpp | 35 +++++------ src/buffer/out/search.h | 10 ++- src/cascadia/TerminalControl/ControlCore.cpp | 65 ++++++++------------ src/cascadia/TerminalControl/ControlCore.h | 5 +- src/cascadia/TerminalControl/ControlCore.idl | 1 - src/cascadia/TerminalControl/TermControl.cpp | 16 +++-- src/host/ut_host/SearchTests.cpp | 40 ++++++------ src/interactivity/win32/find.cpp | 5 +- src/types/UiaTextRangeBase.cpp | 5 +- 9 files changed, 86 insertions(+), 96 deletions(-) diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index 08ed8ad2ef..3fe7ca0c8c 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -8,29 +8,22 @@ using namespace Microsoft::Console::Types; -bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive, std::vector* prevResults) +bool Search::IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive) const noexcept +{ + return _renderData != &renderData || + _needle != needle || + _caseInsensitive != caseInsensitive || + _lastMutationId != renderData.GetTextBuffer().GetLastMutationId(); +} + +bool Search::Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive, bool reverse) { const auto& textBuffer = renderData.GetTextBuffer(); - const auto lastMutationId = textBuffer.GetLastMutationId(); - - if (_renderData == &renderData && - _needle == needle && - _caseInsensitive == caseInsensitive && - _lastMutationId == lastMutationId) - { - _step = reverse ? -1 : 1; - return false; - } - - if (prevResults) - { - *prevResults = std::move(_results); - } _renderData = &renderData; _needle = needle; _caseInsensitive = caseInsensitive; - _lastMutationId = lastMutationId; + _lastMutationId = textBuffer.GetLastMutationId(); _results = textBuffer.SearchText(needle, caseInsensitive); _index = reverse ? gsl::narrow_cast(_results.size()) - 1 : 0; @@ -98,8 +91,9 @@ void Search::MovePastPoint(const til::point anchor) noexcept _index = (index + count) % count; } -void Search::FindNext() noexcept +void Search::FindNext(bool reverse) noexcept { + _step = reverse ? -1 : 1; if (const auto count{ gsl::narrow_cast(_results.size()) }) { _index = (_index + _step + count) % count; @@ -141,6 +135,11 @@ const std::vector& Search::Results() const noexcept return _results; } +std::vector&& Search::ExtractResults() noexcept +{ + return std::move(_results); +} + ptrdiff_t Search::CurrentMatch() const noexcept { return _index; diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index c115b8bdd5..c991eb92e7 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -25,21 +25,19 @@ class Search final public: Search() = default; - bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, - const std::wstring_view& needle, - bool reverse, - bool caseInsensitive, - std::vector* prevResults = nullptr); + bool IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive) const noexcept; + bool Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive, bool reverse); void MoveToCurrentSelection(); void MoveToPoint(til::point anchor) noexcept; void MovePastPoint(til::point anchor) noexcept; - void FindNext() noexcept; + void FindNext(bool reverse) noexcept; const til::point_span* GetCurrent() const noexcept; bool SelectCurrent() const; const std::vector& Results() const noexcept; + std::vector&& ExtractResults() noexcept; ptrdiff_t CurrentMatch() const noexcept; private: diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 7e2d13ee7d..02e1af5802 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1654,30 +1654,41 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - text: the text to search // - goForward: boolean that represents if the current search direction is forward // - caseSensitive: boolean that represents if the current search is case sensitive + // - resetOnly: If true, only Reset() will be called, if anything. FindNext() will never be called. // Return Value: // - - SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool reset) + SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool resetOnly) { const auto lock = _terminal->LockForWriting(); + const auto searchInvalidated = _searcher.IsStale(*_terminal.get(), text, !caseSensitive); - bool searchInvalidated = false; - std::vector oldResults; - if (_searcher.ResetIfStale(*GetRenderData(), text, !goForward, !caseSensitive, &oldResults)) + if (searchInvalidated || !resetOnly) { - searchInvalidated = true; + std::vector oldResults; - _cachedSearchResultRows = {}; - if (SnapSearchResultToSelection()) + if (searchInvalidated) { - _searcher.MoveToCurrentSelection(); - SnapSearchResultToSelection(false); + oldResults = _searcher.ExtractResults(); + _searcher.Reset(*_terminal.get(), text, !caseSensitive, !goForward); + + if (SnapSearchResultToSelection()) + { + _searcher.MoveToCurrentSelection(); + SnapSearchResultToSelection(false); + } + + _terminal->SetSearchHighlights(_searcher.Results()); + } + else + { + _searcher.FindNext(!goForward); } - _terminal->SetSearchHighlights(_searcher.Results()); - } - else if (!reset) - { - _searcher.FindNext(); + if (const auto idx = _searcher.CurrentMatch(); idx >= 0) + { + _terminal->SetSearchHighlightFocused(gsl::narrow(idx)); + } + _renderer->TriggerSearchHighlight(oldResults); } int32_t totalMatches = 0; @@ -1686,11 +1697,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { totalMatches = gsl::narrow(_searcher.Results().size()); currentMatch = gsl::narrow(idx); - _terminal->SetSearchHighlightFocused(gsl::narrow(idx)); } - _renderer->TriggerSearchHighlight(oldResults); - return { .TotalMatches = totalMatches, .CurrentMatch = currentMatch, @@ -1698,27 +1706,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation }; } - Windows::Foundation::Collections::IVector ControlCore::SearchResultRows() + const std::vector& ControlCore::SearchResultRows() const noexcept { - if (!_cachedSearchResultRows) - { - auto results = std::vector(); - auto lastRow = til::CoordTypeMin; - - for (const auto& match : _searcher.Results()) - { - const auto row{ match.start.y }; - if (row != lastRow) - { - results.push_back(row); - lastRow = row; - } - } - - _cachedSearchResultRows = winrt::single_threaded_vector(std::move(results)); - } - - return _cachedSearchResultRows; + return _searcher.Results(); } void ControlCore::ClearSearch() @@ -1728,7 +1718,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetSearchHighlightFocused({}); _renderer->TriggerSearchHighlight(_searcher.Results()); _searcher = {}; - _cachedSearchResultRows = {}; } // Method Description: diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 76b5d8ed95..1eb4b532c4 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -220,12 +220,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetEndSelectionPoint(const til::point position); SearchResults Search(const std::wstring_view& text, bool goForward, bool caseSensitive, bool reset); + const std::vector& SearchResultRows() const noexcept; void ClearSearch(); void SnapSearchResultToSelection(bool snap) noexcept; bool SnapSearchResultToSelection() const noexcept; - Windows::Foundation::Collections::IVector SearchResultRows(); - void LeftClickOnTerminal(const til::point terminalPosition, const int numberOfClicks, const bool altEnabled, @@ -353,8 +352,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::point _contextMenuBufferPosition{ 0, 0 }; - Windows::Foundation::Collections::IVector _cachedSearchResultRows{ nullptr }; - void _setupDispatcherAndCallbacks(); bool _setFontSizeUnderLock(float fontSize); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index ac5eed7345..d10a963786 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -136,7 +136,6 @@ namespace Microsoft.Terminal.Control SearchResults Search(String text, Boolean goForward, Boolean caseSensitive, Boolean reset); void ClearSearch(); - IVector SearchResultRows { get; }; Boolean SnapSearchResultToSelection; Microsoft.Terminal.Core.Color ForegroundColor { get; }; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 6502be591e..e68810d192 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -495,14 +495,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_searchBox && _searchBox->Visibility() == Visibility::Visible) { - if (const auto searchMatches = _core.SearchResultRows()) - { - const til::color color{ _core.ForegroundColor() }; - const auto rightAlignedOffset = (scrollBarWidthInPx - pipWidth) * sizeof(til::color); + const auto core = winrt::get_self(_core); + const auto& searchMatches = core->SearchResultRows(); + const auto color = core->ForegroundColor(); + const auto rightAlignedOffset = (scrollBarWidthInPx - pipWidth) * sizeof(til::color); + til::CoordType lastRow = til::CoordTypeMin; - for (const auto row : searchMatches) + for (const auto& span : searchMatches) + { + if (lastRow != span.start.y) { - const auto base = dataAt(row) + rightAlignedOffset; + lastRow = span.start.y; + const auto base = dataAt(lastRow) + rightAlignedOffset; drawPip(base, color); } } diff --git a/src/host/ut_host/SearchTests.cpp b/src/host/ut_host/SearchTests.cpp index 5a2e4b9bd6..29b3692db0 100644 --- a/src/host/ut_host/SearchTests.cpp +++ b/src/host/ut_host/SearchTests.cpp @@ -57,7 +57,7 @@ class SearchTests return true; } - static void DoFoundChecks(Search& s, til::point coordStartExpected, til::CoordType lineDelta) + static void DoFoundChecks(Search& s, til::point coordStartExpected, til::CoordType lineDelta, bool reverse) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -70,7 +70,7 @@ class SearchTests coordStartExpected.y += lineDelta; coordEndExpected.y += lineDelta; - s.FindNext(); + s.FindNext(reverse); VERIFY_IS_TRUE(s.SelectCurrent()); VERIFY_ARE_EQUAL(coordStartExpected, gci.renderData.GetSelectionAnchor()); @@ -78,7 +78,7 @@ class SearchTests coordStartExpected.y += lineDelta; coordEndExpected.y += lineDelta; - s.FindNext(); + s.FindNext(reverse); VERIFY_IS_TRUE(s.SelectCurrent()); VERIFY_ARE_EQUAL(coordStartExpected, gci.renderData.GetSelectionAnchor()); @@ -86,7 +86,7 @@ class SearchTests coordStartExpected.y += lineDelta; coordEndExpected.y += lineDelta; - s.FindNext(); + s.FindNext(reverse); VERIFY_IS_TRUE(s.SelectCurrent()); VERIFY_ARE_EQUAL(coordStartExpected, gci.renderData.GetSelectionAnchor()); @@ -98,16 +98,16 @@ class SearchTests auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"AB", false, false); - DoFoundChecks(s, {}, 1); + s.Reset(gci.renderData, L"AB", false, false); + DoFoundChecks(s, {}, 1, false); } TEST_METHOD(ForwardCaseSensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"\x304b", false, false); - DoFoundChecks(s, { 2, 0 }, 1); + s.Reset(gci.renderData, L"\x304b", false, false); + DoFoundChecks(s, { 2, 0 }, 1, false); } TEST_METHOD(ForwardCaseInsensitive) @@ -115,47 +115,47 @@ class SearchTests auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"ab", false, true); - DoFoundChecks(s, {}, 1); + s.Reset(gci.renderData, L"ab", true, false); + DoFoundChecks(s, {}, 1, false); } TEST_METHOD(ForwardCaseInsensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"\x304b", false, true); - DoFoundChecks(s, { 2, 0 }, 1); + s.Reset(gci.renderData, L"\x304b", true, false); + DoFoundChecks(s, { 2, 0 }, 1, false); } TEST_METHOD(BackwardCaseSensitive) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"AB", true, false); - DoFoundChecks(s, { 0, 3 }, -1); + s.Reset(gci.renderData, L"AB", false, true); + DoFoundChecks(s, { 0, 3 }, -1, true); } TEST_METHOD(BackwardCaseSensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"\x304b", true, false); - DoFoundChecks(s, { 2, 3 }, -1); + s.Reset(gci.renderData, L"\x304b", false, true); + DoFoundChecks(s, { 2, 3 }, -1, true); } TEST_METHOD(BackwardCaseInsensitive) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"ab", true, true); - DoFoundChecks(s, { 0, 3 }, -1); + s.Reset(gci.renderData, L"ab", true, true); + DoFoundChecks(s, { 0, 3 }, -1, true); } TEST_METHOD(BackwardCaseInsensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Search s; - s.ResetIfStale(gci.renderData, L"\x304b", true, true); - DoFoundChecks(s, { 2, 3 }, -1); + s.Reset(gci.renderData, L"\x304b", true, true); + DoFoundChecks(s, { 2, 3 }, -1, true); } }; diff --git a/src/interactivity/win32/find.cpp b/src/interactivity/win32/find.cpp index 4e7cf36a58..8a0ea0aeb5 100644 --- a/src/interactivity/win32/find.cpp +++ b/src/interactivity/win32/find.cpp @@ -52,13 +52,14 @@ INT_PTR CALLBACK FindDialogProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM l LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - if (searcher.ResetIfStale(gci.renderData, lastFindString, reverse, caseInsensitive)) + if (searcher.IsStale(gci.renderData, lastFindString, caseInsensitive)) { + searcher.Reset(gci.renderData, lastFindString, caseInsensitive, reverse); searcher.MoveToCurrentSelection(); } else { - searcher.FindNext(); + searcher.FindNext(reverse); } if (searcher.SelectCurrent()) diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 788386fa8d..fc21128e1a 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -620,7 +620,10 @@ try // -> We need to turn [_beg,_end) into (_beg,_end). exclusiveBegin.x--; - _searcher.ResetIfStale(*_pData, queryText, searchBackward, ignoreCase); + if (_searcher.IsStale(*_pData, queryText, ignoreCase)) + { + _searcher.Reset(*_pData, queryText, ignoreCase, searchBackward); + } _searcher.MovePastPoint(searchBackward ? _end : exclusiveBegin); til::point hitBeg{ til::CoordTypeMax, til::CoordTypeMax }; From 77087e6282cbab33932198c7d47795c7b9158235 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 1 May 2024 10:06:33 -0500 Subject: [PATCH 32/53] Fix the location that selecting a mark uses (#17138) I think this subtly regressed in #16611. Jump to https://github.com/microsoft/terminal/commit/90b8bb7c2d8932fc601aed9c26f13c77d5a1321d#diff-f9112caf8cb75e7a48a7b84987724d754181227385fbfcc2cc09a879b1f97c12L171-L223 `Terminal::SelectNewRegion` is the only thing that uses the return value from `Terminal::_ScrollToPoints`. Before that PR, `_ScrollToPoints` was just a part of `SelectNewRegion`, and it moved the start & end coords by the `_VisibleStartIndex`, not the `_scrollOffset`. Kinda weird there weren't any _other_ tests for `SelectNewRegion`? I also caught a second bug while I was here - If you had a line with an exact wrap, and tried to select that like with selectOutput, we'd explode. Closes #17131 --- src/cascadia/TerminalControl/ControlCore.cpp | 19 ++- .../TerminalCore/terminalrenderdata.cpp | 2 +- .../UnitTests_Control/ControlCoreTests.cpp | 136 ++++++++++++++++++ 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 02e1af5802..43242d1264 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -2614,12 +2614,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation CompletionsChanged.raise(*this, *args); } + + // Select the region of text between [s.start, s.end), in buffer space void ControlCore::_selectSpan(til::point_span s) { + // s.end is an _exclusive_ point. We need an inclusive one. But + // decrement in bounds wants an inclusive one. If you pass an exclusive + // one, then it might assert at you for being out of bounds. So we also + // take care of the case that the end point is outside the viewport + // manually. const auto bufferSize{ _terminal->GetTextBuffer().GetSize() }; - bufferSize.DecrementInBounds(s.end); + til::point inclusiveEnd = s.end; + if (s.end.x == bufferSize.Width()) + { + inclusiveEnd = til::point{ std::max(0, s.end.x - 1), s.end.y }; + } + else + { + bufferSize.DecrementInBounds(inclusiveEnd); + } - _terminal->SelectNewRegion(s.start, s.end); + _terminal->SelectNewRegion(s.start, inclusiveEnd); _renderer->TriggerSelection(); } diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index ec28b59d56..3d989f31f0 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -199,7 +199,7 @@ til::CoordType Terminal::_ScrollToPoints(const til::point coordStart, const til: _NotifyScrollEvent(); } - return _scrollOffset; + return _VisibleStartIndex(); } void Terminal::SelectNewRegion(const til::point coordStart, const til::point coordEnd) diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index aeefdca995..63f20f9058 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -40,6 +40,8 @@ namespace ControlUnitTests TEST_METHOD(TestSelectCommandSimple); TEST_METHOD(TestSelectOutputSimple); + TEST_METHOD(TestSelectOutputScrolling); + TEST_METHOD(TestSelectOutputExactWrap); TEST_METHOD(TestSimpleClickSelection); @@ -508,6 +510,140 @@ namespace ControlUnitTests } } + void ControlCoreTests::TestSelectOutputScrolling() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = createCore(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print some text"); + + _writePrompt(conn, L"C:\\Windows"); // row 0 + conn->WriteInput(L"Foo-bar"); // row 0 + conn->WriteInput(L"\x1b]133;C\x7"); + + conn->WriteInput(L"\r\n"); + conn->WriteInput(L"This is some text \r\n"); // row 1 + conn->WriteInput(L"with varying amounts \r\n"); // row 2 + conn->WriteInput(L"of whitespace \r\n"); // row 3 + + _writePrompt(conn, L"C:\\Windows"); // row 4 + conn->WriteInput(L"gci"); + conn->WriteInput(L"\x1b]133;C\x7"); + conn->WriteInput(L"\r\n"); + + // enough to scroll + for (auto i = 0; i < 30; i++) // row 5-34 + { + conn->WriteInput(L"-a--- 2/8/2024 9:47 README\r\n"); + } + + _writePrompt(conn, L"C:\\Windows"); + + Log::Comment(L"Check the buffer contents"); + const auto& buffer = core->_terminal->GetTextBuffer(); + const auto& cursor = buffer.GetCursor(); + + { + const til::point expectedCursor{ 17, 35 }; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + } + + VERIFY_IS_FALSE(core->HasSelection()); + + // The second mark is the first one we'll see + core->SelectOutput(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 20, 4 }; // The character after the prompt + const til::point expectedEnd{ 26, 34 }; // x = the end of the text + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + core->SelectOutput(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 24, 0 }; // The character after the prompt + const til::point expectedEnd{ 21, 3 }; // x = the end of the text + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + } + + void ControlCoreTests::TestSelectOutputExactWrap() + { + // Just like the TestSelectOutputScrolling test, but these lines will + // exactly wrap to the right edge of the buffer, to catch a edge case + // present in `ControlCore::_selectSpan` + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = createCore(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print some text"); + + _writePrompt(conn, L"C:\\Windows"); // row 0 + conn->WriteInput(L"Foo-bar"); // row 0 + conn->WriteInput(L"\x1b]133;C\x7"); + + conn->WriteInput(L"\r\n"); + conn->WriteInput(L"This is some text \r\n"); // row 1 + conn->WriteInput(L"with varying amounts \r\n"); // row 2 + conn->WriteInput(L"of whitespace \r\n"); // row 3 + + _writePrompt(conn, L"C:\\Windows"); // row 4 + conn->WriteInput(L"gci"); + conn->WriteInput(L"\x1b]133;C\x7"); + conn->WriteInput(L"\r\n"); + + // enough to scroll + for (auto i = 0; i < 30; i++) // row 5-35 + { + conn->WriteInput(L"-a--- 2/8/2024 9:47 README.md\r\n"); + } + + _writePrompt(conn, L"C:\\Windows"); + + Log::Comment(L"Check the buffer contents"); + const auto& buffer = core->_terminal->GetTextBuffer(); + const auto& cursor = buffer.GetCursor(); + + { + const til::point expectedCursor{ 17, 35 }; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + } + + VERIFY_IS_FALSE(core->HasSelection()); + // The second mark is the first one we'll see + core->SelectOutput(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 20, 4 }; // The character after the prompt + const til::point expectedEnd{ 29, 34 }; // x = the end of the text + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + core->SelectOutput(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 24, 0 }; // The character after the prompt + const til::point expectedEnd{ 21, 3 }; // x = the end of the text + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + } + void ControlCoreTests::TestSimpleClickSelection() { // Create a simple selection with the mouse, then click somewhere else, From d3803943cab9719ba09b8c3bc7dbda1608f5c8e6 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 1 May 2024 19:09:20 +0200 Subject: [PATCH 33/53] Fix font axes/features settings UI (#17164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Due to #16821 everything about #16104 broke. This PR rights the wrongs by rewriting all the `Font`-based code to not use `Font` at all. Instead we split the font spec once into font families, do a lot of complex logic to split font axes/features into used and unused ones and construct all the UI elements. So. much. boilerplate. code. Closes #16943 ## Validation Steps Performed There are more edge cases than I can list here... Some ideas: * Edit the settings.json with invalid axis/feature keys ✅ * ...out of range values ✅ * Settings UI reloads when the settings.json changes ✅ * Adding axes/features works ✅ * Removing axes/features works ✅ * Resetting axes/features works ✅ * Axes/features apply in the renderer when saving ✅ --- doc/cascadia/profiles.schema.json | 4 +- src/cascadia/TerminalControl/ControlCore.cpp | 31 +- .../TerminalControl/ControlSettings.h | 2 +- .../TerminalControl/IControlSettings.idl | 2 +- .../TerminalSettingsEditor/Appearances.cpp | 1269 ++++++++--------- .../TerminalSettingsEditor/Appearances.h | 168 +-- .../TerminalSettingsEditor/Appearances.idl | 59 +- .../TerminalSettingsEditor/Appearances.xaml | 152 +- .../ProfileViewModel.cpp | 34 +- .../TerminalSettingsEditor/ProfileViewModel.h | 7 - .../ProfileViewModel.idl | 4 - .../Resources/en-US/Resources.resw | 6 + .../TerminalSettingsModel/FontConfig.cpp | 23 +- .../TerminalSettingsModel/FontConfig.h | 2 +- .../TerminalSettingsModel/FontConfig.idl | 2 +- .../TerminalSettingsModel/JsonUtils.h | 20 + .../TerminalSettingsModel/TerminalSettings.h | 2 +- .../UnitTests_Control/MockControlSettings.h | 2 +- .../SerializationTests.cpp | 20 +- src/cascadia/ut_app/JsonUtilsTests.cpp | 6 +- src/renderer/atlas/AtlasEngine.api.cpp | 13 +- src/renderer/atlas/AtlasEngine.h | 4 +- 22 files changed, 837 insertions(+), 995 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 34945728c0..d354bf0bcc 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -349,7 +349,7 @@ "description": "Sets the DWrite font features for the given font. For example, { \"ss01\": 1, \"liga\":0 } will enable ss01 and disable ligatures.", "type": "object", "patternProperties": { - "^(([A-Za-z0-9]){4})$": { + "^[\\x20-\\x7E]{4}$": { "type": "integer" } }, @@ -359,7 +359,7 @@ "description": "Sets the DWrite font axes for the given font. For example, { \"wght\": 200 } will set the font weight to 200.", "type": "object", "patternProperties": { - "^([A-Za-z]{4})$": { + "^[\\x20-\\x7E]{4}$": { "type": "number" } }, diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 43242d1264..da78e6167a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -959,26 +959,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_renderEngine) { - std::unordered_map featureMap; - if (const auto fontFeatures = _settings->FontFeatures()) - { - featureMap.reserve(fontFeatures.Size()); - - for (const auto& [tag, param] : fontFeatures) + static constexpr auto cloneMap = [](const IFontFeatureMap& map) { + std::unordered_map clone; + if (map) { - featureMap.emplace(tag, param); + clone.reserve(map.Size()); + for (const auto& [tag, param] : map) + { + clone.emplace(tag, param); + } } - } - std::unordered_map axesMap; - if (const auto fontAxes = _settings->FontAxes()) - { - axesMap.reserve(fontAxes.Size()); + return clone; + }; - for (const auto& [axis, value] : fontAxes) - { - axesMap.emplace(axis, value); - } - } + const auto fontFeatures = _settings->FontFeatures(); + const auto fontAxes = _settings->FontAxes(); + const auto featureMap = cloneMap(fontFeatures); + const auto axesMap = cloneMap(fontAxes); // TODO: MSFT:20895307 If the font doesn't exist, this doesn't // actually fail. We need a way to gracefully fallback. diff --git a/src/cascadia/TerminalControl/ControlSettings.h b/src/cascadia/TerminalControl/ControlSettings.h index 0423b36f7b..75d12b9707 100644 --- a/src/cascadia/TerminalControl/ControlSettings.h +++ b/src/cascadia/TerminalControl/ControlSettings.h @@ -10,7 +10,7 @@ Licensed under the MIT license. #include #include "ControlAppearance.h" -using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; namespace winrt::Microsoft::Terminal::Control::implementation diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index 29cd96b6f8..51eaf34f90 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -40,7 +40,7 @@ namespace Microsoft.Terminal.Control Single FontSize { get; }; Windows.UI.Text.FontWeight FontWeight { get; }; String Padding { get; }; - Windows.Foundation.Collections.IMap FontFeatures { get; }; + Windows.Foundation.Collections.IMap FontFeatures { get; }; Windows.Foundation.Collections.IMap FontAxes { get; }; Boolean EnableBuiltinGlyphs { get; }; Boolean EnableColorGlyphs { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.cpp b/src/cascadia/TerminalSettingsEditor/Appearances.cpp index 036b045ca5..c2823b955b 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.cpp +++ b/src/cascadia/TerminalSettingsEditor/Appearances.cpp @@ -3,13 +3,14 @@ #include "pch.h" #include "Appearances.h" -#include "Appearances.g.cpp" -#include "AxisKeyValuePair.g.cpp" -#include "FeatureKeyValuePair.g.cpp" + +#include +#include "../WinRTUtils/inc/Utils.h" #include "EnumEntry.h" -#include -#include "..\WinRTUtils\inc\Utils.h" +#include "ProfileViewModel.h" + +#include "Appearances.g.cpp" using namespace winrt::Windows::UI::Text; using namespace winrt::Windows::UI::Xaml; @@ -20,299 +21,186 @@ using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Microsoft::Terminal::Settings::Model; -static constexpr std::array DefaultFeatures{ - L"rlig", - L"locl", - L"ccmp", - L"calt", - L"liga", - L"clig", - L"rnrn", - L"kern", - L"mark", - L"mkmk", - L"dist" +// These features are enabled by default by DWrite, so if a user adds them, +// we initialize the setting to a value of 1 instead of 0. +static constexpr std::array s_defaultFeatures{ + DWRITE_MAKE_FONT_FEATURE_TAG('c', 'a', 'l', 't'), + DWRITE_MAKE_FONT_FEATURE_TAG('c', 'c', 'm', 'p'), + DWRITE_MAKE_FONT_FEATURE_TAG('c', 'l', 'i', 'g'), + DWRITE_MAKE_FONT_FEATURE_TAG('d', 'i', 's', 't'), + DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'), + DWRITE_MAKE_FONT_FEATURE_TAG('l', 'i', 'g', 'a'), + DWRITE_MAKE_FONT_FEATURE_TAG('l', 'o', 'c', 'l'), + DWRITE_MAKE_FONT_FEATURE_TAG('m', 'a', 'r', 'k'), + DWRITE_MAKE_FONT_FEATURE_TAG('m', 'k', 'm', 'k'), + DWRITE_MAKE_FONT_FEATURE_TAG('r', 'l', 'i', 'g'), + DWRITE_MAKE_FONT_FEATURE_TAG('r', 'n', 'r', 'n'), }; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { - Windows::Foundation::Collections::IMap Font::FontAxesTagsAndNames() + struct TagToStringImpl { - if (!_fontAxesTagsAndNames) + explicit TagToStringImpl(uint32_t tag) noexcept { - wil::com_ptr font; - THROW_IF_FAILED(_family->GetFont(0, font.put())); - wil::com_ptr fontFace; - THROW_IF_FAILED(font->CreateFontFace(fontFace.put())); - wil::com_ptr fontFace5; - if (fontFace5 = fontFace.try_query()) - { - wil::com_ptr fontResource; - THROW_IF_FAILED(fontFace5->GetFontResource(fontResource.put())); - - const auto axesCount = fontFace5->GetFontAxisValueCount(); - if (axesCount > 0) - { - std::vector axesVector(axesCount); - fontFace5->GetFontAxisValues(axesVector.data(), axesCount); - - uint32_t localeIndex; - BOOL localeExists; - wchar_t localeName[LOCALE_NAME_MAX_LENGTH]; - const auto localeToTry = GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH) ? localeName : L"en-US"; - - std::unordered_map fontAxesTagsAndNames; - for (uint32_t i = 0; i < axesCount; ++i) - { - wil::com_ptr names; - THROW_IF_FAILED(fontResource->GetAxisNames(i, names.put())); - - if (!SUCCEEDED(names->FindLocaleName(localeToTry, &localeIndex, &localeExists)) || !localeExists) - { - // default to the first locale in the list - localeIndex = 0; - } - - UINT32 length = 0; - if (SUCCEEDED(names->GetStringLength(localeIndex, &length))) - { - winrt::impl::hstring_builder builder{ length }; - if (SUCCEEDED(names->GetString(localeIndex, builder.data(), length + 1))) - { - fontAxesTagsAndNames.insert(std::pair(_tagToString(axesVector[i].axisTag), builder.to_hstring())); - continue; - } - } - // if there was no name found, it means the font does not actually support this axis - // don't insert anything into the vector in this case - } - _fontAxesTagsAndNames = winrt::single_threaded_map(std::move(fontAxesTagsAndNames)); - } - } + _buffer[0] = static_cast((tag >> 0) & 0xFF); + _buffer[1] = static_cast((tag >> 8) & 0xFF); + _buffer[2] = static_cast((tag >> 16) & 0xFF); + _buffer[3] = static_cast((tag >> 24) & 0xFF); + _buffer[4] = 0; } - return _fontAxesTagsAndNames; + + operator std::wstring_view() const noexcept + { + return { &_buffer[0], 4 }; + } + + private: + wchar_t _buffer[5]; + }; + + // Turns a DWRITE_MAKE_OPENTYPE_TAG into a string_view... + // (...buffer holder because someone needs to hold onto the data the view refers to.) + static TagToStringImpl tagToString(uint32_t tag) noexcept + { + return TagToStringImpl{ tag }; } - IMap Font::FontFeaturesTagsAndNames() + // Turns a string to a DWRITE_MAKE_OPENTYPE_TAG. Returns 0 on failure. + static uint32_t tagFromString(std::wstring_view str) noexcept { - if (!_fontFeaturesTagsAndNames) + if (str.size() != 4) { - wil::com_ptr font; - THROW_IF_FAILED(_family->GetFont(0, font.put())); - wil::com_ptr fontFace; - THROW_IF_FAILED(font->CreateFontFace(fontFace.put())); - - wil::com_ptr factory; - THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof()))); - wil::com_ptr textAnalyzer; - factory->CreateTextAnalyzer(textAnalyzer.addressof()); - wil::com_ptr textAnalyzer2 = textAnalyzer.query(); - - DWRITE_SCRIPT_ANALYSIS scriptAnalysis{}; - UINT32 tagCount; - // we have to call GetTypographicFeatures twice, first to get the actual count then to get the features - std::ignore = textAnalyzer2->GetTypographicFeatures(fontFace.get(), scriptAnalysis, L"en-us", 0, &tagCount, nullptr); - std::vector tags{ tagCount }; - textAnalyzer2->GetTypographicFeatures(fontFace.get(), scriptAnalysis, L"en-us", tagCount, &tagCount, tags.data()); - - std::unordered_map fontFeaturesTagsAndNames; - for (auto tag : tags) - { - const auto tagString = _tagToString(tag); - hstring formattedResourceString{ fmt::format(L"Profile_FontFeature_{}", tagString) }; - hstring localizedName{ tagString }; - // we have resource strings for common font features, see if one for this feature exists - if (HasLibraryResourceWithName(formattedResourceString)) - { - localizedName = GetLibraryResourceString(formattedResourceString); - } - fontFeaturesTagsAndNames.insert(std::pair(tagString, localizedName)); - } - _fontFeaturesTagsAndNames = winrt::single_threaded_map(std::move(fontFeaturesTagsAndNames)); + return 0; } - return _fontFeaturesTagsAndNames; - } - winrt::hstring Font::_tagToString(DWRITE_FONT_AXIS_TAG tag) - { - std::wstring result; + // Check if all 4 characters are printable ASCII. for (int i = 0; i < 4; ++i) { - result.push_back((tag >> (i * 8)) & 0xFF); - } - return winrt::hstring{ result }; - } - - hstring Font::_tagToString(DWRITE_FONT_FEATURE_TAG tag) - { - std::wstring result; - for (int i = 0; i < 4; ++i) - { - result.push_back((tag >> (i * 8)) & 0xFF); - } - return hstring{ result }; - } - - AxisKeyValuePair::AxisKeyValuePair(winrt::hstring axisKey, float axisValue, const Windows::Foundation::Collections::IMap& baseMap, const Windows::Foundation::Collections::IMap& tagToNameMap) : - _AxisKey{ axisKey }, - _AxisValue{ axisValue }, - _baseMap{ baseMap }, - _tagToNameMap{ tagToNameMap } - { - if (_tagToNameMap.HasKey(_AxisKey)) - { - int32_t i{ 0 }; - // IMap guarantees that the iteration order is the same every time - // so this conversion of key to index is safe - for (const auto tagAndName : _tagToNameMap) + const auto ch = str[i]; + if (ch < 0x20 || ch > 0x7E) { - if (tagAndName.Key() == _AxisKey) - { - _AxisIndex = i; - break; - } - ++i; + return 0; } } + + return DWRITE_MAKE_OPENTYPE_TAG(str[0], str[1], str[2], str[3]); } - winrt::hstring AxisKeyValuePair::AxisKey() + static winrt::hstring getLocalizedStringByIndex(IDWriteLocalizedStrings* strings, UINT32 index) { - return _AxisKey; + UINT32 length = 0; + THROW_IF_FAILED(strings->GetStringLength(index, &length)); + + winrt::impl::hstring_builder builder{ length }; + THROW_IF_FAILED(strings->GetString(index, builder.data(), length + 1)); + + return builder.to_hstring(); } - float AxisKeyValuePair::AxisValue() + static UINT32 getLocalizedStringIndex(IDWriteLocalizedStrings* strings, const wchar_t* locale, UINT32 fallback) { - return _AxisValue; - } - - int32_t AxisKeyValuePair::AxisIndex() - { - return _AxisIndex; - } - - void AxisKeyValuePair::AxisValue(float axisValue) - { - if (axisValue != _AxisValue) + UINT32 index; + BOOL exists; + if (FAILED(strings->FindLocaleName(locale, &index, &exists)) || !exists) { - _baseMap.Remove(_AxisKey); - _AxisValue = axisValue; - _baseMap.Insert(_AxisKey, _AxisValue); - PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"AxisValue" }); + index = fallback; } + return index; } - void AxisKeyValuePair::AxisKey(winrt::hstring axisKey) + Font::Font(winrt::hstring name, winrt::hstring localizedName) : + _Name{ std::move(name) }, + _LocalizedName{ std::move(localizedName) } { - if (axisKey != _AxisKey) + } + + bool FontKeyValuePair::SortAscending(const Editor::FontKeyValuePair& lhs, const Editor::FontKeyValuePair& rhs) + { + const auto& a = winrt::get_self(lhs)->KeyDisplayStringRef(); + const auto& b = winrt::get_self(rhs)->KeyDisplayStringRef(); + return til::compare_linguistic_insensitive(a, b) < 0; + } + + FontKeyValuePair::FontKeyValuePair(winrt::weak_ref vm, winrt::hstring keyDisplayString, uint32_t key, float value, bool isFontFeature) : + _vm{ std::move(vm) }, + _keyDisplayString{ std::move(keyDisplayString) }, + _key{ key }, + _value{ value }, + _isFontFeature{ isFontFeature } + { + } + + uint32_t FontKeyValuePair::Key() const noexcept + { + return _key; + } + + winrt::hstring FontKeyValuePair::KeyDisplayString() + { + return KeyDisplayStringRef(); + } + + // You can't return a const-ref from a WinRT function, because the cppwinrt generated wrapper chokes on it. + // So, now we got two KeyDisplayString() functions, because I refuse to AddRef/Release this for no reason. + // I mean, really it makes no perf. difference, but I'm not kneeling down for an incompetent code generator. + const winrt::hstring& FontKeyValuePair::KeyDisplayStringRef() + { + if (!_keyDisplayString.empty()) { - _baseMap.Remove(_AxisKey); - _AxisKey = axisKey; - _baseMap.Insert(_AxisKey, _AxisValue); - PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"AxisKey" }); + return _keyDisplayString; } - } - void AxisKeyValuePair::AxisIndex(int32_t axisIndex) - { - if (axisIndex != _AxisIndex) + const auto tagString = tagToString(_key); + hstring displayString; + + if (_isFontFeature) { - _AxisIndex = axisIndex; - - int32_t i{ 0 }; - // same as in the constructor, iterating through IMap - // gives us the same order every time - for (const auto tagAndName : _tagToNameMap) + const auto key = fmt::format(FMT_COMPILE(L"Profile_FontFeature_{}"), std::wstring_view{ tagString }); + if (HasLibraryResourceWithName(key)) { - if (i == _AxisIndex) - { - AxisKey(tagAndName.Key()); - break; - } - ++i; + displayString = GetLibraryResourceString(key); + displayString = hstring{ fmt::format(FMT_COMPILE(L"{} ({})"), displayString, std::wstring_view{ tagString }) }; } } + + if (displayString.empty()) + { + displayString = hstring{ tagString }; + } + + _keyDisplayString = displayString; + return _keyDisplayString; } - FeatureKeyValuePair::FeatureKeyValuePair(hstring featureKey, uint32_t featureValue, const IMap& baseMap, const IMap& tagToNameMap) : - _FeatureKey{ featureKey }, - _FeatureValue{ featureValue }, - _baseMap{ baseMap }, - _tagToNameMap{ tagToNameMap } + float FontKeyValuePair::Value() const noexcept { - if (_tagToNameMap.HasKey(_FeatureKey)) + return _value; + } + + void FontKeyValuePair::Value(float v) + { + if (_value == v) { - int32_t i{ 0 }; - // this loop assumes that every time we iterate through the map - // we get the same ordering - for (const auto tagAndName : _tagToNameMap) - { - if (tagAndName.Key() == _FeatureKey) - { - _FeatureIndex = i; - break; - } - ++i; - } + return; + } + + _value = v; + + if (const auto vm = _vm.get()) + { + vm->UpdateFontSetting(this); } } - hstring FeatureKeyValuePair::FeatureKey() + void FontKeyValuePair::SetValueDirect(float v) { - return _FeatureKey; + _value = v; } - uint32_t FeatureKeyValuePair::FeatureValue() + bool FontKeyValuePair::IsFontFeature() const noexcept { - return _FeatureValue; - } - - int32_t FeatureKeyValuePair::FeatureIndex() - { - return _FeatureIndex; - } - - void FeatureKeyValuePair::FeatureValue(uint32_t featureValue) - { - if (featureValue != _FeatureValue) - { - _baseMap.Remove(_FeatureKey); - _FeatureValue = featureValue; - _baseMap.Insert(_FeatureKey, _FeatureValue); - PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FeatureValue" }); - } - } - - void FeatureKeyValuePair::FeatureKey(hstring featureKey) - { - if (featureKey != _FeatureKey) - { - _baseMap.Remove(_FeatureKey); - _FeatureKey = featureKey; - _baseMap.Insert(_FeatureKey, _FeatureValue); - PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FeatureKey" }); - } - } - - void FeatureKeyValuePair::FeatureIndex(int32_t featureIndex) - { - if (featureIndex != _FeatureIndex) - { - _FeatureIndex = featureIndex; - - int32_t i{ 0 }; - // same as in the constructor, this assumes that iterating through the map - // gives us the same order every time - for (const auto tagAndName : _tagToNameMap) - { - if (i == _FeatureIndex) - { - FeatureKey(tagAndName.Key()); - break; - } - ++i; - } - } + return _isFontFeature; } AppearanceViewModel::AppearanceViewModel(const Model::AppearanceConfig& appearance) : @@ -333,25 +221,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // box, prevent it from ever being changed again. _NotifyChanges(L"UseDesktopBGImage", L"BackgroundImageSettingsVisible"); } - else if (viewModelProperty == L"FontAxes") - { - // this is a weird one - // we manually make the observable vector based on the map in the settings model - // (this is due to xaml being unable to bind a list view to a map) - // so when the FontAxes change (say from the reset button), reinitialize the observable vector - InitializeFontAxesVector(); - } - else if (viewModelProperty == L"FontFeatures") - { - // same as the FontAxes one - InitializeFontFeaturesVector(); - } }); - _refreshFontFaceDependents(); - InitializeFontAxesVector(); - InitializeFontFeaturesVector(); - // Cache the original BG image path. If the user clicks "Use desktop // wallpaper", then un-checks it, this is the string we'll restore to // them. @@ -375,7 +246,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } fontInfo.FontFace(value); - _refreshFontFaceDependents(); + _invalidateFontFaceDependents(); _NotifyChanges(L"HasFontFace", L"FontFace"); } @@ -388,15 +259,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void AppearanceViewModel::ClearFontFace() { const auto fontInfo = _appearance.SourceProfile().FontInfo(); - const auto hadValue = fontInfo.HasFontFace(); fontInfo.ClearFontFace(); - _refreshFontFaceDependents(); + _invalidateFontFaceDependents(); - if (hadValue) - { - _NotifyChanges(L"HasFontFace", L"FontFace"); - } + _NotifyChanges(L"HasFontFace", L"FontFace"); } Model::FontConfig AppearanceViewModel::FontFaceOverrideSource() const @@ -412,50 +279,62 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation wil::com_ptr fontCollection; THROW_IF_FAILED(factory->GetSystemFontCollection(fontCollection.addressof(), FALSE)); - const auto fontFace = FontFace(); - std::wstring primaryFontName; + const auto fontFaceSpec = FontFace(); std::wstring missingFonts; std::wstring proportionalFonts; + std::array, 2> fontSettingsRemaining; BOOL hasPowerlineCharacters = FALSE; - til::iterate_font_families(fontFace, [&](wil::zwstring_view name) { - if (primaryFontName.empty()) - { - primaryFontName = name; - } + wchar_t localeNameBuffer[LOCALE_NAME_MAX_LENGTH]; + const auto localeName = GetUserDefaultLocaleName(localeNameBuffer, LOCALE_NAME_MAX_LENGTH) ? localeNameBuffer : L"en-US"; + til::iterate_font_families(fontFaceSpec, [&](wil::zwstring_view name) { std::wstring* accumulator = nullptr; - UINT32 index = 0; - BOOL exists = FALSE; - THROW_IF_FAILED(fontCollection->FindFamilyName(name.c_str(), &index, &exists)); - - // Look ma, no goto! - do + try { - if (!exists) + UINT32 index = 0; + BOOL exists = FALSE; + THROW_IF_FAILED(fontCollection->FindFamilyName(name.c_str(), &index, &exists)); + + // Look ma, no goto! + do { - accumulator = &missingFonts; - break; - } + if (!exists) + { + accumulator = &missingFonts; + break; + } - wil::com_ptr fontFamily; - THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.addressof())); + wil::com_ptr fontFamily; + THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.addressof())); - wil::com_ptr font; - THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof())); + wil::com_ptr font; + THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof())); - if (!font.query()->IsMonospacedFont()) - { - accumulator = &proportionalFonts; - } + if (!font.query()->IsMonospacedFont()) + { + accumulator = &proportionalFonts; + } - // We're actually checking for the "Extended" PowerLine glyph set. - // They're more fun. - BOOL hasE0B6 = FALSE; - std::ignore = font->HasCharacter(0xE0B6, &hasE0B6); - hasPowerlineCharacters |= hasE0B6; - } while (false); + // We're actually checking for the "Extended" PowerLine glyph set. + // They're more fun. + BOOL hasE0B6 = FALSE; + std::ignore = font->HasCharacter(0xE0B6, &hasE0B6); + hasPowerlineCharacters |= hasE0B6; + + wil::com_ptr fontFace; + THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof())); + + _generateFontAxes(fontFace.get(), localeName, fontSettingsRemaining[FontAxesIndex]); + _generateFontFeatures(fontFace.get(), fontSettingsRemaining[FontFeaturesIndex]); + } while (false); + } + catch (...) + { + accumulator = &missingFonts; + LOG_CAUGHT_EXCEPTION(); + } if (accumulator) { @@ -467,10 +346,212 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } }); - _primaryFontName = std::move(primaryFontName); - MissingFontFaces(winrt::hstring{ missingFonts }); - ProportionalFontFaces(winrt::hstring{ proportionalFonts }); - HasPowerlineCharacters(hasPowerlineCharacters); + // Up to this point, our two vectors are sorted by tag value. We want to sort them by display string now, + // because this will result in sorted fontSettingsUsed/Unused lists below. + for (auto& v : fontSettingsRemaining) + { + std::sort(v.begin(), v.end(), FontKeyValuePair::SortAscending); + } + + std::array, 2> fontSettingsUsed; + const std::array fontSettingsUser{ + _appearance.SourceProfile().FontInfo().FontAxes(), + _appearance.SourceProfile().FontInfo().FontFeatures(), + }; + + // Find all axes and features that are in the user settings, and move them to the used list. + // They'll be displayed as a list in the UI. + for (int i = FontAxesIndex; i <= FontFeaturesIndex; i++) + { + const auto& map = fontSettingsUser[i]; + if (!map) + { + continue; + } + + for (const auto& [tagString, value] : fontSettingsUser[i]) + { + const auto tag = tagFromString(tagString); + if (!tag) + { + continue; + } + + auto& remaining = fontSettingsRemaining[i]; + const auto it = std::ranges::find_if(remaining, [&](const Editor::FontKeyValuePair& kv) { + return winrt::get_self(kv)->Key() == tag; + }); + + Editor::FontKeyValuePair kv{ nullptr }; + if (it != remaining.end()) + { + kv = std::move(*it); + remaining.erase(it); + + const auto kvImpl = winrt::get_self(kv); + kvImpl->SetValueDirect(value); + } + else + { + kv = winrt::make(get_weak(), hstring{}, tag, value, i == FontFeaturesIndex); + } + + fontSettingsUsed[i].emplace_back(std::move(kv)); + } + } + + std::array, 2> fontSettingsUnused; + + // All remaining (= unused) axes and features are turned into menu items. + // They'll be displayed as a flyout when clicking the "add item" button. + for (int i = FontAxesIndex; i <= FontFeaturesIndex; i++) + { + for (const auto& kv : fontSettingsRemaining[i]) + { + fontSettingsUnused[i].emplace_back(_createFontSettingMenuItem(kv)); + } + } + + auto& d = _fontFaceDependents.emplace(); + d.missingFontFaces = winrt::hstring{ missingFonts }; + d.proportionalFontFaces = winrt::hstring{ proportionalFonts }; + d.hasPowerlineCharacters = hasPowerlineCharacters; + + d.fontSettingsUsed[FontAxesIndex] = winrt::single_threaded_observable_vector(std::move(fontSettingsUsed[FontAxesIndex])); + d.fontSettingsUsed[FontFeaturesIndex] = winrt::single_threaded_observable_vector(std::move(fontSettingsUsed[FontFeaturesIndex])); + d.fontSettingsUnused = std::move(fontSettingsUnused); + + _notifyChangesForFontSettings(); + } + + std::pair::const_iterator, bool> AppearanceViewModel::_fontSettingSortedByKeyInsertPosition(const std::vector& vec, uint32_t key) + { + const auto it = std::lower_bound(vec.begin(), vec.end(), key, [](const Editor::FontKeyValuePair& lhs, uint32_t rhs) { + return winrt::get_self(lhs)->Key() < rhs; + }); + const auto exists = it != vec.end() && winrt::get_self(*it)->Key() == key; + return { it, exists }; + } + + void AppearanceViewModel::_generateFontAxes(IDWriteFontFace* fontFace, const wchar_t* localeName, std::vector& list) + { + const auto fontFace5 = wil::try_com_query(fontFace); + if (!fontFace5) + { + return; + } + + const auto axesCount = fontFace5->GetFontAxisValueCount(); + if (axesCount == 0) + { + return; + } + + std::vector axesVector(axesCount); + THROW_IF_FAILED(fontFace5->GetFontAxisValues(axesVector.data(), axesCount)); + + wil::com_ptr fontResource; + THROW_IF_FAILED(fontFace5->GetFontResource(fontResource.addressof())); + + for (UINT32 i = 0; i < axesCount; ++i) + { + wil::com_ptr names; + THROW_IF_FAILED(fontResource->GetAxisNames(i, names.addressof())); + + // As per MSDN: + // > The font author may not have supplied names for some font axes. + // > The localized strings will be empty in that case. + if (names->GetCount() == 0) + { + continue; + } + + const auto tag = axesVector[i].axisTag; + const auto [it, tagExists] = _fontSettingSortedByKeyInsertPosition(list, tag); + if (tagExists) + { + continue; + } + + UINT32 index; + BOOL exists; + if (FAILED(names->FindLocaleName(localeName, &index, &exists)) || !exists) + { + index = 0; + } + + const auto idx = getLocalizedStringIndex(names.get(), localeName, 0); + const auto localizedName = getLocalizedStringByIndex(names.get(), idx); + const auto tagString = tagToString(tag); + hstring displayString{ fmt::format(FMT_COMPILE(L"{} ({})"), localizedName, std::wstring_view{ tagString }) }; + + const auto value = axesVector[i].value; + + list.emplace(it, winrt::make(get_weak(), std::move(displayString), tag, value, false)); + } + } + + void AppearanceViewModel::_generateFontFeatures(IDWriteFontFace* fontFace, std::vector& list) + { + wil::com_ptr factory; + THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof()))); + + wil::com_ptr textAnalyzer; + THROW_IF_FAILED(factory->CreateTextAnalyzer(textAnalyzer.addressof())); + const auto textAnalyzer2 = textAnalyzer.query(); + + static constexpr DWRITE_SCRIPT_ANALYSIS scriptAnalysis{}; + UINT32 tagCount; + if (textAnalyzer2->GetTypographicFeatures(fontFace, scriptAnalysis, L"en-US", 0, &tagCount, nullptr) != E_NOT_SUFFICIENT_BUFFER) + { + return; + } + std::vector tags{ tagCount }; + if (FAILED(textAnalyzer2->GetTypographicFeatures(fontFace, scriptAnalysis, L"en-US", tagCount, &tagCount, tags.data()))) + { + return; + } + + for (const auto& tag : tags) + { + const auto [it, tagExists] = _fontSettingSortedByKeyInsertPosition(list, tag); + if (tagExists) + { + continue; + } + + const auto dfBeg = s_defaultFeatures.begin(); + const auto dfEnd = s_defaultFeatures.end(); + const auto isDefaultFeature = std::find(dfBeg, dfEnd, tag) != dfEnd; + const auto value = isDefaultFeature ? 1.0f : 0.0f; + + list.emplace(it, winrt::make(get_weak(), hstring{}, tag, value, true)); + } + } + + MenuFlyoutItemBase AppearanceViewModel::_createFontSettingMenuItem(const Editor::FontKeyValuePair& kv) + { + const auto kvImpl = winrt::get_self(kv); + + MenuFlyoutItem item; + item.Text(kvImpl->KeyDisplayStringRef()); + item.Click([weakSelf = get_weak(), kv](const IInspectable& sender, const RoutedEventArgs&) { + if (const auto self = weakSelf.get()) + { + self->AddFontKeyValuePair(sender, kv); + } + }); + return item; + } + + void AppearanceViewModel::_notifyChangesForFontSettings() + { + _NotifyChanges( + L"FontFaceDependents", + L"FontAxes", + L"FontFeatures", + L"HasFontAxes", + L"HasFontFeatures"); } double AppearanceViewModel::LineHeight() const @@ -535,6 +616,180 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation FontWeight(winrt::Microsoft::Terminal::UI::Converters::DoubleToFontWeight(fontWeight)); } + IObservableVector AppearanceViewModel::FontAxes() + { + return FontFaceDependents().fontSettingsUsed[FontAxesIndex]; + } + + bool AppearanceViewModel::HasFontAxes() const + { + return _appearance.SourceProfile().FontInfo().HasFontAxes(); + } + + void AppearanceViewModel::ClearFontAxes() + { + _deleteAllFontSettings(FontAxesIndex); + } + + Model::FontConfig AppearanceViewModel::FontAxesOverrideSource() const + { + return _appearance.SourceProfile().FontInfo().FontAxesOverrideSource(); + } + + IObservableVector AppearanceViewModel::FontFeatures() + { + return FontFaceDependents().fontSettingsUsed[FontFeaturesIndex]; + } + + bool AppearanceViewModel::HasFontFeatures() const + { + return _appearance.SourceProfile().FontInfo().HasFontFeatures(); + } + + void AppearanceViewModel::ClearFontFeatures() + { + _deleteAllFontSettings(FontFeaturesIndex); + } + + Model::FontConfig AppearanceViewModel::FontFeaturesOverrideSource() const + { + return _appearance.SourceProfile().FontInfo().FontFeaturesOverrideSource(); + } + + void AppearanceViewModel::AddFontKeyValuePair(const IInspectable& sender, const Editor::FontKeyValuePair& kv) + { + if (!_fontFaceDependents) + { + return; + } + + const auto kvImpl = winrt::get_self(kv); + const auto fontSettingsIndex = kvImpl->IsFontFeature() ? 1 : 0; + auto& d = *_fontFaceDependents; + auto& used = d.fontSettingsUsed[fontSettingsIndex]; + auto& unused = d.fontSettingsUnused[fontSettingsIndex]; + + const auto it = std::ranges::find(unused, sender); + if (it == unused.end()) + { + return; + } + + // Sync the added value into the user settings model. + UpdateFontSetting(kvImpl); + + // Insert the item into the used list, keeping it sorted by the display text. + { + const auto it = std::lower_bound(used.begin(), used.end(), kv, FontKeyValuePair::SortAscending); + used.InsertAt(gsl::narrow(it - used.begin()), kv); + } + + unused.erase(it); + + _notifyChangesForFontSettings(); + } + + void AppearanceViewModel::DeleteFontKeyValuePair(const Editor::FontKeyValuePair& kv) + { + if (!_fontFaceDependents) + { + return; + } + + const auto kvImpl = winrt::get_self(kv); + const auto tag = kvImpl->Key(); + const auto tagString = tagToString(tag); + const auto fontSettingsIndex = kvImpl->IsFontFeature() ? 1 : 0; + auto& d = *_fontFaceDependents; + auto& used = d.fontSettingsUsed[fontSettingsIndex]; + auto& unused = d.fontSettingsUnused[fontSettingsIndex]; + + const auto fontInfo = _appearance.SourceProfile().FontInfo(); + auto fontSettingsUser = kvImpl->IsFontFeature() ? fontInfo.FontFeatures() : fontInfo.FontAxes(); + if (!fontSettingsUser) + { + return; + } + + const auto it = std::ranges::find(used, kv); + if (it == used.end()) + { + return; + } + + fontSettingsUser.Remove(std::wstring_view{ tagString }); + + // Insert the item into the unused list, keeping it sorted by the display text. + { + const auto item = _createFontSettingMenuItem(*it); + const auto it = std::lower_bound(unused.begin(), unused.end(), item, [](const MenuFlyoutItemBase& lhs, const MenuFlyoutItemBase& rhs) { + const auto& a = lhs.as().Text(); + const auto& b = rhs.as().Text(); + return til::compare_linguistic_insensitive(a, b) < 0; + }); + unused.insert(it, item); + } + + used.RemoveAt(gsl::narrow(it - used.begin())); + + _notifyChangesForFontSettings(); + } + + void AppearanceViewModel::UpdateFontSetting(const FontKeyValuePair* kvImpl) + { + const auto tag = kvImpl->Key(); + const auto value = kvImpl->Value(); + const auto tagString = tagToString(tag); + const auto fontInfo = _appearance.SourceProfile().FontInfo(); + auto fontSettingsUser = kvImpl->IsFontFeature() ? fontInfo.FontFeatures() : fontInfo.FontAxes(); + + if (!fontSettingsUser) + { + fontSettingsUser = winrt::single_threaded_map(); + if (kvImpl->IsFontFeature()) + { + fontInfo.FontFeatures(fontSettingsUser); + } + else + { + fontInfo.FontAxes(fontSettingsUser); + } + } + + std::ignore = fontSettingsUser.Insert(std::wstring_view{ tagString }, value); + } + + void AppearanceViewModel::_deleteAllFontSettings(FontSettingIndex fontSettingsIndex) + { + const auto fontInfo = _appearance.SourceProfile().FontInfo(); + if (fontSettingsIndex == FontFeaturesIndex) + { + fontInfo.ClearFontFeatures(); + } + else + { + fontInfo.ClearFontAxes(); + } + + if (!_fontFaceDependents) + { + return; + } + + auto& d = *_fontFaceDependents; + auto& used = d.fontSettingsUsed[fontSettingsIndex]; + auto& unused = d.fontSettingsUnused[fontSettingsIndex]; + + for (const auto& kv : used) + { + unused.emplace_back(_createFontSettingMenuItem(kv)); + } + + used.Clear(); + + _notifyChangesForFontSettings(); + } + void AppearanceViewModel::SetBackgroundImageOpacityFromPercentageValue(double percentageValue) { BackgroundImageOpacity(static_cast(percentageValue) / 100.0f); @@ -607,263 +862,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation LightColorSchemeName(val.Name()); } - void AppearanceViewModel::AddNewAxisKeyValuePair() - { - if (!_appearance.SourceProfile().FontInfo().FontAxes()) - { - _appearance.SourceProfile().FontInfo().FontAxes(winrt::single_threaded_map()); - } - auto fontAxesMap = _appearance.SourceProfile().FontInfo().FontAxes(); - - // find one axis that does not already exist, and add that - // if there are no more possible axes to add, the button is disabled so there shouldn't be a way to get here - const auto possibleAxesTagsAndNames = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames(); - for (const auto tagAndName : possibleAxesTagsAndNames) - { - if (!fontAxesMap.HasKey(tagAndName.Key())) - { - fontAxesMap.Insert(tagAndName.Key(), gsl::narrow(0)); - FontAxesVector().Append(_CreateAxisKeyValuePairHelper(tagAndName.Key(), gsl::narrow(0), fontAxesMap, possibleAxesTagsAndNames)); - break; - } - } - _NotifyChanges(L"CanFontAxesBeAdded"); - } - - void AppearanceViewModel::DeleteAxisKeyValuePair(winrt::hstring key) - { - for (uint32_t i = 0; i < _FontAxesVector.Size(); i++) - { - if (_FontAxesVector.GetAt(i).AxisKey() == key) - { - FontAxesVector().RemoveAt(i); - _appearance.SourceProfile().FontInfo().FontAxes().Remove(key); - if (_FontAxesVector.Size() == 0) - { - _appearance.SourceProfile().FontInfo().ClearFontAxes(); - } - break; - } - } - _NotifyChanges(L"CanFontAxesBeAdded"); - } - - void AppearanceViewModel::InitializeFontAxesVector() - { - if (!_FontAxesVector) - { - _FontAxesVector = winrt::single_threaded_observable_vector(); - } - - _FontAxesVector.Clear(); - if (const auto fontAxesMap = _appearance.SourceProfile().FontInfo().FontAxes()) - { - const auto fontAxesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames(); - for (const auto axis : fontAxesMap) - { - // only show the axes that the font supports - // any axes that the font doesn't support continue to be stored in the json, we just don't show them in the UI - if (fontAxesTagToNameMap.HasKey(axis.Key())) - { - _FontAxesVector.Append(_CreateAxisKeyValuePairHelper(axis.Key(), axis.Value(), fontAxesMap, fontAxesTagToNameMap)); - } - } - } - _NotifyChanges(L"AreFontAxesAvailable", L"CanFontAxesBeAdded"); - } - - // Method Description: - // - Determines whether the currently selected font has any variable font axes - bool AppearanceViewModel::AreFontAxesAvailable() - { - return ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames().Size() > 0; - } - - // Method Description: - // - Determines whether the currently selected font has any variable font axes that have not already been set - bool AppearanceViewModel::CanFontAxesBeAdded() - { - if (const auto fontAxesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames(); fontAxesTagToNameMap.Size() > 0) - { - if (const auto fontAxesMap = _appearance.SourceProfile().FontInfo().FontAxes()) - { - for (const auto tagAndName : fontAxesTagToNameMap) - { - if (!fontAxesMap.HasKey(tagAndName.Key())) - { - // we found an axis that has not been set - return true; - } - } - // all possible axes have been set already - return false; - } - // the font supports font axes but the profile has none set - return true; - } - // the font does not support any font axes - return false; - } - - // Method Description: - // - Creates an AxisKeyValuePair and sets up an event handler for it - Editor::AxisKeyValuePair AppearanceViewModel::_CreateAxisKeyValuePairHelper(winrt::hstring axisKey, float axisValue, const Windows::Foundation::Collections::IMap& baseMap, const Windows::Foundation::Collections::IMap& tagToNameMap) - { - const auto axisKeyValuePair = winrt::make(axisKey, axisValue, baseMap, tagToNameMap); - // when either the key or the value changes, send an event for the preview control to catch - axisKeyValuePair.PropertyChanged([weakThis = get_weak()](auto& /*sender*/, auto& /*e*/) { - if (auto appVM{ weakThis.get() }) - { - appVM->_NotifyChanges(L"AxisKeyValuePair"); - } - }); - return axisKeyValuePair; - } - - void AppearanceViewModel::AddNewFeatureKeyValuePair() - { - const auto fontInfo = _appearance.SourceProfile().FontInfo(); - auto fontFeaturesMap = fontInfo.FontFeatures(); - if (!fontFeaturesMap) - { - fontFeaturesMap = winrt::single_threaded_map(); - fontInfo.FontFeatures(fontFeaturesMap); - } - - // find one feature that does not already exist, and add that - // if there are no more possible features to add, the button is disabled so there shouldn't be a way to get here - const auto possibleFeaturesTagsAndNames = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames(); - for (const auto tagAndName : possibleFeaturesTagsAndNames) - { - const auto featureKey = tagAndName.Key(); - if (!fontFeaturesMap.HasKey(featureKey)) - { - const auto featureDefaultValue = _IsDefaultFeature(featureKey) ? 1 : 0; - fontFeaturesMap.Insert(featureKey, featureDefaultValue); - FontFeaturesVector().Append(_CreateFeatureKeyValuePairHelper(featureKey, featureDefaultValue, fontFeaturesMap, possibleFeaturesTagsAndNames)); - break; - } - } - _NotifyChanges(L"CanFontFeaturesBeAdded"); - } - - void AppearanceViewModel::DeleteFeatureKeyValuePair(hstring key) - { - for (uint32_t i = 0; i < _FontFeaturesVector.Size(); i++) - { - if (_FontFeaturesVector.GetAt(i).FeatureKey() == key) - { - FontFeaturesVector().RemoveAt(i); - _appearance.SourceProfile().FontInfo().FontFeatures().Remove(key); - if (_FontFeaturesVector.Size() == 0) - { - _appearance.SourceProfile().FontInfo().ClearFontFeatures(); - } - break; - } - } - _NotifyChanges(L"CanFontAxesBeAdded"); - } - - void AppearanceViewModel::InitializeFontFeaturesVector() - { - if (!_FontFeaturesVector) - { - _FontFeaturesVector = single_threaded_observable_vector(); - } - - _FontFeaturesVector.Clear(); - if (const auto fontFeaturesMap = _appearance.SourceProfile().FontInfo().FontFeatures()) - { - const auto fontFeaturesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames(); - for (const auto feature : fontFeaturesMap) - { - const auto featureKey = feature.Key(); - // only show the features that the font supports - // any features that the font doesn't support continue to be stored in the json, we just don't show them in the UI - if (fontFeaturesTagToNameMap.HasKey(featureKey)) - { - _FontFeaturesVector.Append(_CreateFeatureKeyValuePairHelper(featureKey, feature.Value(), fontFeaturesMap, fontFeaturesTagToNameMap)); - } - } - } - _NotifyChanges(L"AreFontFeaturesAvailable", L"CanFontFeaturesBeAdded"); - } - - // Method Description: - // - Determines whether the currently selected font has any font features - bool AppearanceViewModel::AreFontFeaturesAvailable() - { - return ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames().Size() > 0; - } - - // Method Description: - // - Determines whether the currently selected font has any font features that have not already been set - bool AppearanceViewModel::CanFontFeaturesBeAdded() - { - if (const auto fontFeaturesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames(); fontFeaturesTagToNameMap.Size() > 0) - { - if (const auto fontFeaturesMap = _appearance.SourceProfile().FontInfo().FontFeatures()) - { - for (const auto tagAndName : fontFeaturesTagToNameMap) - { - if (!fontFeaturesMap.HasKey(tagAndName.Key())) - { - // we found a feature that has not been set - return true; - } - } - // all possible features have been set already - return false; - } - // the font supports font features but the profile has none set - return true; - } - // the font does not support any font features - return false; - } - - // Method Description: - // - Creates a FeatureKeyValuePair and sets up an event handler for it - Editor::FeatureKeyValuePair AppearanceViewModel::_CreateFeatureKeyValuePairHelper(hstring featureKey, uint32_t featureValue, const IMap& baseMap, const IMap& tagToNameMap) - { - const auto featureKeyValuePair = winrt::make(featureKey, featureValue, baseMap, tagToNameMap); - // when either the key or the value changes, send an event for the preview control to catch - featureKeyValuePair.PropertyChanged([weakThis = get_weak()](auto& sender, const PropertyChangedEventArgs& args) { - if (auto appVM{ weakThis.get() }) - { - appVM->_NotifyChanges(L"FeatureKeyValuePair"); - const auto settingName{ args.PropertyName() }; - if (settingName == L"FeatureKey") - { - const auto senderPair = sender.as(); - const auto senderKey = senderPair->FeatureKey(); - if (appVM->_IsDefaultFeature(senderKey)) - { - senderPair->FeatureValue(1); - } - else - { - senderPair->FeatureValue(0); - } - } - } - }); - return featureKeyValuePair; - } - - bool AppearanceViewModel::_IsDefaultFeature(winrt::hstring featureKey) - { - for (const auto defaultFeature : DefaultFeatures) - { - if (defaultFeature == featureKey) - { - return true; - } - } - return false; - } - DependencyProperty Appearances::_AppearanceProperty{ nullptr }; Appearances::Appearances() @@ -928,12 +926,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto backgroundImgCheckboxTooltip{ ToolTipService::GetToolTip(UseDesktopImageCheckBox()) }; Automation::AutomationProperties::SetFullDescription(UseDesktopImageCheckBox(), unbox_value(backgroundImgCheckboxTooltip)); - _FontAxesNames = winrt::single_threaded_observable_vector(); - FontAxesNamesCVS().Source(_FontAxesNames); - - _FontFeaturesNames = winrt::single_threaded_observable_vector(); - FontFeaturesNamesCVS().Source(_FontFeaturesNames); - INITIALIZE_BINDABLE_ENUM_SETTING(IntenseTextStyle, IntenseTextStyle, winrt::Microsoft::Terminal::Settings::Model::IntenseStyle, L"Appearance_IntenseTextStyle", L"Content"); } @@ -983,55 +975,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { appearance.FontFace(fontSpec); } - - // TODO: Any use of FindFontWithLocalizedName is broken and requires refactoring in time for version 1.21. - const auto newFontFace = ProfileViewModel::FindFontWithLocalizedName(fontSpec); - if (!newFontFace) - { - return; - } - - _FontAxesNames.Clear(); - const auto axesTagsAndNames = newFontFace.FontAxesTagsAndNames(); - for (const auto tagAndName : axesTagsAndNames) - { - _FontAxesNames.Append(tagAndName.Value()); - } - - _FontFeaturesNames.Clear(); - const auto featuresTagsAndNames = newFontFace.FontFeaturesTagsAndNames(); - for (const auto tagAndName : featuresTagsAndNames) - { - _FontFeaturesNames.Append(tagAndName.Value()); - } - - // when the font face changes, we have to tell the view model to update the font axes/features vectors - // since the new font may not have the same possible axes as the previous one - Appearance().InitializeFontAxesVector(); - if (!Appearance().AreFontAxesAvailable()) - { - // if the previous font had available font axes and the expander was expanded, - // at this point the expander would be set to disabled so manually collapse it - FontAxesContainer().SetExpanded(false); - FontAxesContainer().HelpText(RS_(L"Profile_FontAxesUnavailable/Text")); - } - else - { - FontAxesContainer().HelpText(RS_(L"Profile_FontAxesAvailable/Text")); - } - - Appearance().InitializeFontFeaturesVector(); - if (!Appearance().AreFontFeaturesAvailable()) - { - // if the previous font had available font features and the expander was expanded, - // at this point the expander would be set to disabled so manually collapse it - FontFeaturesContainer().SetExpanded(false); - FontFeaturesContainer().HelpText(RS_(L"Profile_FontFeaturesUnavailable/Text")); - } - else - { - FontFeaturesContainer().HelpText(RS_(L"Profile_FontFeaturesAvailable/Text")); - } } void Appearances::FontFaceBox_SuggestionChosen(const AutoSuggestBox& sender, const AutoSuggestBoxSuggestionChosenEventArgs& args) @@ -1127,17 +1070,30 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { if (const auto appearance = Appearance()) { - const auto& biAlignmentVal{ static_cast(appearance.BackgroundImageAlignment()) }; + const auto appearanceImpl = winrt::get_self(appearance); + const auto& biAlignmentVal{ static_cast(appearanceImpl->BackgroundImageAlignment()) }; for (const auto& biButton : _BIAlignmentButtons) { biButton.IsChecked(biButton.Tag().as() == biAlignmentVal); } - FontAxesCVS().Source(Appearance().FontAxesVector()); - FontAxesContainer().HelpText(appearance.AreFontAxesAvailable() ? RS_(L"Profile_FontAxesAvailable/Text") : RS_(L"Profile_FontAxesUnavailable/Text")); + { + const auto& d = appearanceImpl->FontFaceDependents(); - FontFeaturesCVS().Source(Appearance().FontFeaturesVector()); - FontFeaturesContainer().HelpText(appearance.AreFontFeaturesAvailable() ? RS_(L"Profile_FontFeaturesAvailable/Text") : RS_(L"Profile_FontFeaturesUnavailable/Text")); + const std::array buttons{ + AddFontAxisButton(), + AddFontFeatureButton(), + }; + + for (int i = 0; i < 2; ++i) + { + const auto& button = buttons[i]; + const auto& data = d.fontSettingsUnused[i]; + const auto items = button.Flyout().as().Items(); + items.ReplaceAll(data); + button.IsEnabled(!data.empty()); + } + } _ViewModelChangedRevoker = appearance.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) { const auto settingName{ args.PropertyName() }; @@ -1156,13 +1112,36 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (settingName == L"BackgroundImageAlignment") { - _UpdateBIAlignmentControl(static_cast(appearance.BackgroundImageAlignment())); + _UpdateBIAlignmentControl(static_cast(appearanceImpl->BackgroundImageAlignment())); } else if (settingName == L"FontWeight") { PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentFontWeight" }); PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsCustomFontWeight" }); } + else if (settingName == L"FontFaceDependents") + { + const auto& d = appearanceImpl->FontFaceDependents(); + + const std::array buttons{ + AddFontAxisButton(), + AddFontFeatureButton(), + }; + + for (int i = 0; i < 2; ++i) + { + const auto& button = buttons[i]; + const auto& data = d.fontSettingsUnused[i]; + const auto flyout = button.Flyout().as(); + const auto items = flyout.Items(); + items.ReplaceAll(data); + button.IsEnabled(!data.empty()); + // WinUI doesn't hide the flyout when it's currently open and the items are now empty. + // In fact it doesn't close it, period. You click an item? Flyout stays open! + // This gets called whenever an item is selected, so it's the "perfect" time to close it. + flyout.Hide(); + } + } else if (settingName == L"IntenseTextStyle") { PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentIntenseTextStyle" }); @@ -1248,36 +1227,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } - void Appearances::DeleteAxisKeyValuePair_Click(const IInspectable& sender, const RoutedEventArgs& /*e*/) + void Appearances::DeleteFontKeyValuePair_Click(const IInspectable& sender, const RoutedEventArgs& /*e*/) { - if (const auto& button{ sender.try_as() }) - { - if (const auto& tag{ button.Tag().try_as() }) - { - Appearance().DeleteAxisKeyValuePair(tag.value()); - } - } - } - - void Appearances::AddNewAxisKeyValuePair_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/) - { - Appearance().AddNewAxisKeyValuePair(); - } - - void Appearances::DeleteFeatureKeyValuePair_Click(const IInspectable& sender, const RoutedEventArgs& /*e*/) - { - if (const auto& button{ sender.try_as() }) - { - if (const auto& tag{ button.Tag().try_as() }) - { - Appearance().DeleteFeatureKeyValuePair(tag.value()); - } - } - } - - void Appearances::AddNewFeatureKeyValuePair_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/) - { - Appearance().AddNewFeatureKeyValuePair(); + const auto element = sender.as(); + const auto tag = element.Tag(); + const auto kv = tag.as(); + winrt::get_self(Appearance())->DeleteFontKeyValuePair(kv); } bool Appearances::IsVintageCursor() const diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.h b/src/cascadia/TerminalSettingsEditor/Appearances.h index d01819b24b..572b624098 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.h +++ b/src/cascadia/TerminalSettingsEditor/Appearances.h @@ -17,8 +17,7 @@ Author(s): #pragma once #include "Font.g.h" -#include "AxisKeyValuePair.g.h" -#include "FeatureKeyValuePair.g.h" +#include "FontKeyValuePair.g.h" #include "Appearances.g.h" #include "AppearanceViewModel.g.h" #include "Utils.h" @@ -28,81 +27,57 @@ Author(s): namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { + struct AppearanceViewModel; + struct Font : FontT { - public: - Font(winrt::hstring name, winrt::hstring localizedName, wil::com_ptr family) : - _Name{ std::move(name) }, - _LocalizedName{ std::move(localizedName) }, - _family{ std::move(family) } - { - } - - hstring ToString() { return _LocalizedName; } - Windows::Foundation::Collections::IMap FontAxesTagsAndNames(); - Windows::Foundation::Collections::IMap FontFeaturesTagsAndNames(); + Font(winrt::hstring name, winrt::hstring localizedName); WINRT_PROPERTY(hstring, Name); WINRT_PROPERTY(hstring, LocalizedName); - - private: - winrt::hstring _tagToString(DWRITE_FONT_AXIS_TAG tag); - winrt::hstring _tagToString(DWRITE_FONT_FEATURE_TAG tag); - - Windows::Foundation::Collections::IMap _fontAxesTagsAndNames; - Windows::Foundation::Collections::IMap _fontFeaturesTagsAndNames; - wil::com_ptr _family; }; - struct AxisKeyValuePair : AxisKeyValuePairT, ViewModelHelper + struct FontKeyValuePair : FontKeyValuePairT { - AxisKeyValuePair(winrt::hstring axisKey, float axisValue, const Windows::Foundation::Collections::IMap& baseMap, const Windows::Foundation::Collections::IMap& tagToNameMap); + static bool SortAscending(const Editor::FontKeyValuePair& lhs, const Editor::FontKeyValuePair& rhs); - winrt::hstring AxisKey(); - void AxisKey(winrt::hstring axisKey); + FontKeyValuePair(winrt::weak_ref vm, winrt::hstring keyDisplayString, uint32_t key, float value, bool isFontFeature); - float AxisValue(); - void AxisValue(float axisValue); + winrt::hstring KeyDisplayString(); + const winrt::hstring& KeyDisplayStringRef(); + uint32_t Key() const noexcept; + float Value() const noexcept; + void Value(float v); - int32_t AxisIndex(); - void AxisIndex(int32_t axisIndex); - - til::property_changed_event PropertyChanged; + void SetValueDirect(float v); + bool IsFontFeature() const noexcept; private: - winrt::hstring _AxisKey; - float _AxisValue; - int32_t _AxisIndex; - Windows::Foundation::Collections::IMap _baseMap{ nullptr }; - Windows::Foundation::Collections::IMap _tagToNameMap{ nullptr }; - }; - - struct FeatureKeyValuePair : FeatureKeyValuePairT, ViewModelHelper - { - FeatureKeyValuePair(winrt::hstring featureKey, uint32_t featureValue, const Windows::Foundation::Collections::IMap& baseMap, const Windows::Foundation::Collections::IMap& tagToNameMap); - - winrt::hstring FeatureKey(); - void FeatureKey(winrt::hstring featureKey); - - uint32_t FeatureValue(); - void FeatureValue(uint32_t featureValue); - - int32_t FeatureIndex(); - void FeatureIndex(int32_t featureIndex); - - til::property_changed_event PropertyChanged; - - private: - winrt::hstring _FeatureKey; - uint32_t _FeatureValue; - int32_t _FeatureIndex; - Windows::Foundation::Collections::IMap _baseMap{ nullptr }; - Windows::Foundation::Collections::IMap _tagToNameMap{ nullptr }; + winrt::weak_ref _vm; + winrt::hstring _keyDisplayString; + uint32_t _key; + float _value; + bool _isFontFeature; }; struct AppearanceViewModel : AppearanceViewModelT, ViewModelHelper { - public: + enum FontSettingIndex + { + FontAxesIndex, + FontFeaturesIndex, + }; + + struct FontFaceDependentsData + { + winrt::hstring missingFontFaces; + winrt::hstring proportionalFontFaces; + bool hasPowerlineCharacters = false; + + std::array, 2> fontSettingsUsed; + std::array, 2> fontSettingsUnused; + }; + AppearanceViewModel(const Model::AppearanceConfig& appearance); winrt::hstring FontFace() const; @@ -118,32 +93,46 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Model::FontConfig LineHeightOverrideSource() const; void SetFontWeightFromDouble(double fontWeight); - void SetBackgroundImageOpacityFromPercentageValue(double percentageValue); - void SetBackgroundImagePath(winrt::hstring path); + + const FontFaceDependentsData& FontFaceDependents() + { + if (!_fontFaceDependents) + { + _refreshFontFaceDependents(); + } + return *_fontFaceDependents; + } + + winrt::hstring MissingFontFaces() { return FontFaceDependents().missingFontFaces; } + winrt::hstring ProportionalFontFaces() { return FontFaceDependents().proportionalFontFaces; } + bool HasPowerlineCharacters() { return FontFaceDependents().hasPowerlineCharacters; } + + Windows::Foundation::Collections::IObservableVector FontAxes(); + bool HasFontAxes() const; + void ClearFontAxes(); + Model::FontConfig FontAxesOverrideSource() const; + + Windows::Foundation::Collections::IObservableVector FontFeatures(); + bool HasFontFeatures() const; + void ClearFontFeatures(); + Model::FontConfig FontFeaturesOverrideSource() const; + + void AddFontKeyValuePair(const IInspectable& sender, const Editor::FontKeyValuePair& kv); + void DeleteFontKeyValuePair(const Editor::FontKeyValuePair& kv); + void UpdateFontSetting(const FontKeyValuePair* kv); // background image bool UseDesktopBGImage(); void UseDesktopBGImage(const bool useDesktop); bool BackgroundImageSettingsVisible(); + void SetBackgroundImageOpacityFromPercentageValue(double percentageValue); + void SetBackgroundImagePath(winrt::hstring path); void ClearColorScheme(); Editor::ColorSchemeViewModel CurrentColorScheme(); void CurrentColorScheme(const Editor::ColorSchemeViewModel& val); - void AddNewAxisKeyValuePair(); - void DeleteAxisKeyValuePair(winrt::hstring key); - void InitializeFontAxesVector(); - bool AreFontAxesAvailable(); - bool CanFontAxesBeAdded(); - - void AddNewFeatureKeyValuePair(); - void DeleteFeatureKeyValuePair(winrt::hstring key); - void InitializeFontFeaturesVector(); - bool AreFontFeaturesAvailable(); - bool CanFontFeaturesBeAdded(); - WINRT_PROPERTY(bool, IsDefault, false); - WINRT_PROPERTY(bool, HasPowerlineCharacters, false); // These settings are not defined in AppearanceConfig, so we grab them // from the source profile itself. The reason we still want them in the @@ -152,8 +141,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // are defined in AppearanceConfig and some that are not. OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontSize); OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontWeight); - OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontAxes); - OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontFeatures); OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), EnableBuiltinGlyphs); OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), EnableColorGlyphs); @@ -169,28 +156,24 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation OBSERVABLE_PROJECTED_SETTING(_appearance, IntenseTextStyle); OBSERVABLE_PROJECTED_SETTING(_appearance, AdjustIndistinguishableColors); WINRT_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector, SchemesList, _propertyChangedHandlers, nullptr); - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, MissingFontFaces, _propertyChangedHandlers); - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, ProportionalFontFaces, _propertyChangedHandlers); - WINRT_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector, FontAxesVector, _propertyChangedHandlers, nullptr); - WINRT_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector, FontFeaturesVector, _propertyChangedHandlers, nullptr); private: + void _invalidateFontFaceDependents() { _fontFaceDependents.reset(); } void _refreshFontFaceDependents(); - Editor::AxisKeyValuePair _CreateAxisKeyValuePairHelper(winrt::hstring axisKey, float axisValue, const Windows::Foundation::Collections::IMap& baseMap, const Windows::Foundation::Collections::IMap& tagToNameMap); - Editor::FeatureKeyValuePair _CreateFeatureKeyValuePairHelper(winrt::hstring axisKey, uint32_t axisValue, const Windows::Foundation::Collections::IMap& baseMap, const Windows::Foundation::Collections::IMap& tagToNameMap); - bool _IsDefaultFeature(winrt::hstring featureTag); + static std::pair::const_iterator, bool> _fontSettingSortedByKeyInsertPosition(const std::vector& vec, uint32_t key); + void _generateFontAxes(IDWriteFontFace* fontFace, const wchar_t* localeName, std::vector& list); + void _generateFontFeatures(IDWriteFontFace* fontFace, std::vector& list); + Windows::UI::Xaml::Controls::MenuFlyoutItemBase _createFontSettingMenuItem(const Editor::FontKeyValuePair& kv); + void _notifyChangesForFontSettings(); + void _deleteAllFontSettings(FontSettingIndex index); Model::AppearanceConfig _appearance; winrt::hstring _lastBgImagePath; - std::wstring _primaryFontName; + std::optional _fontFaceDependents; }; struct Appearances : AppearancesT { - public: - static winrt::hstring FontAxisName(const winrt::hstring& key); - static winrt::hstring FontFeatureName(const winrt::hstring& key); - Appearances(); // CursorShape visibility logic @@ -204,12 +187,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void FontFaceBox_LostFocus(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); void FontFaceBox_SuggestionChosen(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox&, const winrt::Windows::UI::Xaml::Controls::AutoSuggestBoxSuggestionChosenEventArgs&); void FontFaceBox_TextChanged(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox&, const winrt::Windows::UI::Xaml::Controls::AutoSuggestBoxTextChangedEventArgs&); + void DeleteFontKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); fire_and_forget BackgroundImage_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); void BIAlignment_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); - void DeleteAxisKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); - void AddNewAxisKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); - void DeleteFeatureKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); - void AddNewFeatureKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); // manually bind FontWeight Windows::Foundation::IInspectable CurrentFontWeight() const; @@ -253,6 +233,4 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation { BASIC_FACTORY(Appearances); - BASIC_FACTORY(AxisKeyValuePair); - BASIC_FACTORY(FeatureKeyValuePair); } diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.idl b/src/cascadia/TerminalSettingsEditor/Appearances.idl index 2855fa21b7..0907e5f8f3 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.idl +++ b/src/cascadia/TerminalSettingsEditor/Appearances.idl @@ -17,30 +17,17 @@ import "ColorSchemesPageViewModel.idl"; namespace Microsoft.Terminal.Settings.Editor { - runtimeclass Font : Windows.Foundation.IStringable + runtimeclass Font { String Name { get; }; String LocalizedName { get; }; - Windows.Foundation.Collections.IMap FontAxesTagsAndNames { get; }; - Windows.Foundation.Collections.IMap FontFeaturesTagsAndNames { get; }; } - // We have to make this because we cannot bind an IObservableMap to a ListView in XAML (in c++) - // So instead we make an IObservableVector of these AxisKeyValuePair objects - runtimeclass AxisKeyValuePair : Windows.UI.Xaml.Data.INotifyPropertyChanged + runtimeclass FontKeyValuePair { - AxisKeyValuePair(String axisKey, Single axisValue, Windows.Foundation.Collections.IMap baseMap, Windows.Foundation.Collections.IMap tagToNameMap); - String AxisKey; - Single AxisValue; - Int32 AxisIndex; - } - - runtimeclass FeatureKeyValuePair : Windows.UI.Xaml.Data.INotifyPropertyChanged - { - FeatureKeyValuePair(String featureKey, UInt32 featureValue, Windows.Foundation.Collections.IMap baseMap, Windows.Foundation.Collections.IMap tagToNameMap); - String FeatureKey; - UInt32 FeatureValue; - Int32 FeatureIndex; + UInt32 Key { get; }; + String KeyDisplayString { get; }; + Single Value; } runtimeclass AppearanceViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged @@ -56,27 +43,21 @@ namespace Microsoft.Terminal.Settings.Editor void ClearColorScheme(); ColorSchemeViewModel CurrentColorScheme; - Windows.Foundation.Collections.IObservableVector SchemesList; + IObservableVector SchemesList; String MissingFontFaces { get; }; String ProportionalFontFaces { get; }; Boolean HasPowerlineCharacters { get; }; - void AddNewAxisKeyValuePair(); - void DeleteAxisKeyValuePair(String key); - void InitializeFontAxesVector(); - Boolean AreFontAxesAvailable { get; }; - Boolean CanFontAxesBeAdded { get; }; - Windows.Foundation.Collections.IObservableVector FontAxesVector; - OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Windows.Foundation.Collections.IMap, FontAxes); + IObservableVector FontAxes { get; }; + Boolean HasFontAxes { get; }; + void ClearFontAxes(); + Object FontAxesOverrideSource { get; }; - void AddNewFeatureKeyValuePair(); - void DeleteFeatureKeyValuePair(String key); - void InitializeFontFeaturesVector(); - Boolean AreFontFeaturesAvailable { get; }; - Boolean CanFontFeaturesBeAdded { get; }; - Windows.Foundation.Collections.IObservableVector FontFeaturesVector; - OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Windows.Foundation.Collections.IMap, FontFeatures); + IObservableVector FontFeatures { get; }; + Boolean HasFontFeatures { get; }; + void ClearFontFeatures(); + Object FontFeaturesOverrideSource { get; }; OBSERVABLE_PROJECTED_APPEARANCE_SETTING(String, FontFace); OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Single, FontSize); @@ -106,24 +87,24 @@ namespace Microsoft.Terminal.Settings.Editor IHostedInWindow WindowRoot; static Windows.UI.Xaml.DependencyProperty AppearanceProperty { get; }; - Windows.Foundation.Collections.IObservableVector FilteredFontList { get; }; + IObservableVector FilteredFontList { get; }; Boolean ShowAllFonts; IInspectable CurrentCursorShape; Boolean IsVintageCursor { get; }; - Windows.Foundation.Collections.IObservableVector CursorShapeList { get; }; + IObservableVector CursorShapeList { get; }; IInspectable CurrentAdjustIndistinguishableColors; - Windows.Foundation.Collections.IObservableVector AdjustIndistinguishableColorsList { get; }; + IObservableVector AdjustIndistinguishableColorsList { get; }; IInspectable CurrentBackgroundImageStretchMode; - Windows.Foundation.Collections.IObservableVector BackgroundImageStretchModeList { get; }; + IObservableVector BackgroundImageStretchModeList { get; }; IInspectable CurrentFontWeight; Boolean IsCustomFontWeight { get; }; - Windows.Foundation.Collections.IObservableVector FontWeightList { get; }; + IObservableVector FontWeightList { get; }; IInspectable CurrentIntenseTextStyle; - Windows.Foundation.Collections.IObservableVector IntenseTextStyleList { get; }; + IObservableVector IntenseTextStyleList { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml index 7d065dc4c9..2f94cf4473 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml +++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml @@ -38,10 +38,29 @@ Background="{x:Bind mtu:Converters.ColorToBrush(Color)}" CornerRadius="1" /> - - + + + + + + + + + + + + + @@ -190,8 +209,7 @@ Visibility="{x:Bind Appearance.IsDefault, Mode=OneWay}"> - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -721,8 +670,7 @@ - GetFamilyNames(familyNames.addressof())); // If en-us is missing we fall back to whatever is at index 0. - const auto ci = getLocalizedStringIndex(familyNames.get(), L"en-us", 0); + const auto ci = getLocalizedStringIndex(familyNames.get(), L"en-US", 0); // If our locale is missing we fall back to en-us. const auto li = getLocalizedStringIndex(familyNames.get(), locale, ci); @@ -242,7 +212,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // If the canonical/localized indices are the same, there's no need to get the other string. auto localized = ci == li ? canonical : getLocalizedStringByIndex(familyNames.get(), li); - return make(std::move(canonical), std::move(localized), family); + return make(std::move(canonical), std::move(localized)); } winrt::guid ProfileViewModel::OriginalProfileGuid() const noexcept diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h index 04225a46ef..f0ce84f1f0 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -33,7 +33,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation static void UpdateFontList() noexcept; static Windows::Foundation::Collections::IObservableVector CompleteFontList() noexcept { return _FontList; }; static Windows::Foundation::Collections::IObservableVector MonospaceFontList() noexcept { return _MonospaceFontList; }; - static Editor::Font FindFontWithLocalizedName(winrt::hstring const& name) noexcept; ProfileViewModel(const Model::Profile& profile, const Model::CascadiaSettings& settings); Model::TerminalSettings TermSettings() const; @@ -156,9 +155,3 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation guid _ProfileGuid{}; }; }; - -namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation -{ - // Since we have static functions, we need a factory. - BASIC_FACTORY(ProfileViewModel); -} diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl index 315afdfb4f..c0bc407c7a 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -34,10 +34,6 @@ namespace Microsoft.Terminal.Settings.Editor runtimeclass ProfileViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged { - static Windows.Foundation.Collections.IObservableVector CompleteFontList { get; }; - static Windows.Foundation.Collections.IObservableVector MonospaceFontList { get; }; - static Font FindFontWithLocalizedName(String name); - Microsoft.Terminal.Settings.Model.TerminalSettings TermSettings { get; }; event Windows.Foundation.TypedEventHandler DeleteProfileRequested; diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 405734f241..f27f774c55 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -733,10 +733,16 @@ Browse... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Add new font axis + Add new Button label that adds a new font axis for the current font. + + Add new font feature + Add new Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.cpp b/src/cascadia/TerminalSettingsModel/FontConfig.cpp index fd9a181ae3..746ceee649 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/FontConfig.cpp @@ -32,24 +32,21 @@ winrt::com_ptr FontConfig::CopyFontInfo(const FontConfig* source, wi // We cannot simply copy the font axes and features with `fontInfo->_FontAxes = source->_FontAxes;` // since that'll just create a reference; we have to manually copy the values. + static constexpr auto cloneFontMap = [](const IFontFeatureMap& map) { + std::map fontAxes; + for (const auto& [k, v] : map) + { + fontAxes.emplace(k, v); + } + return winrt::single_threaded_map(std::move(fontAxes)); + }; if (source->_FontAxes) { - std::map fontAxes; - for (const auto keyValuePair : source->_FontAxes.value()) - { - fontAxes.insert(std::pair(keyValuePair.Key(), keyValuePair.Value())); - } - fontInfo->_FontAxes = winrt::single_threaded_map(std::move(fontAxes)); + fontInfo->_FontAxes = cloneFontMap(*source->_FontAxes); } - if (source->_FontFeatures) { - std::map fontFeatures; - for (const auto keyValuePair : source->_FontFeatures.value()) - { - fontFeatures.insert(std::pair(keyValuePair.Key(), keyValuePair.Value())); - } - fontInfo->_FontFeatures = winrt::single_threaded_map(std::move(fontFeatures)); + fontInfo->_FontFeatures = cloneFontMap(*source->_FontFeatures); } return fontInfo; diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.h b/src/cascadia/TerminalSettingsModel/FontConfig.h index 8a789278eb..c249f5a1b4 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.h +++ b/src/cascadia/TerminalSettingsModel/FontConfig.h @@ -24,7 +24,7 @@ Author(s): #include using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; -using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.idl b/src/cascadia/TerminalSettingsModel/FontConfig.idl index 9145919c43..0303b0c27e 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.idl +++ b/src/cascadia/TerminalSettingsModel/FontConfig.idl @@ -18,7 +18,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_FONT_SETTING(String, FontFace); INHERITABLE_FONT_SETTING(Single, FontSize); INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight); - INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontFeatures); + INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontFeatures); INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontAxes); INHERITABLE_FONT_SETTING(Boolean, EnableBuiltinGlyphs); INHERITABLE_FONT_SETTING(Boolean, EnableColorGlyphs); diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index ec94d23c92..e08047bf0e 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -731,6 +731,16 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils Json::Value ToJson(const float val) { + // Convert floats that are almost integers to proper integers, because that looks way neater in JSON. + if (val >= static_cast(Json::Value::minInt) && val <= static_cast(Json::Value::maxInt)) + { + const auto i = static_cast(std::lround(val)); + const auto f = static_cast(i); + if (std::fabs(f - val) < 1e-6f) + { + return i; + } + } return val; } @@ -755,6 +765,16 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils Json::Value ToJson(const double val) { + // Convert floats that are almost integers to proper integers, because that looks way neater in JSON. + if (val >= static_cast(Json::Value::minInt) && val <= static_cast(Json::Value::maxInt)) + { + const auto i = static_cast(std::lround(val)); + const auto f = static_cast(i); + if (std::fabs(f - val) < 1e-6) + { + return i; + } + } return val; } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 45b4db2aa4..33658427f9 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -21,7 +21,7 @@ Author(s): #include using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; -using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; using IEnvironmentVariableMap = winrt::Windows::Foundation::Collections::IMap; // fwdecl unittest classes diff --git a/src/cascadia/UnitTests_Control/MockControlSettings.h b/src/cascadia/UnitTests_Control/MockControlSettings.h index 9a9ebcda74..50b71687e8 100644 --- a/src/cascadia/UnitTests_Control/MockControlSettings.h +++ b/src/cascadia/UnitTests_Control/MockControlSettings.h @@ -8,7 +8,7 @@ Licensed under the MIT license. #include #include "../../inc/ControlProperties.h" -using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; namespace ControlUnitTests diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index 82f9f924de..2035765663 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -151,7 +151,7 @@ namespace SettingsModelUnitTests "font": { "face": "Cascadia Mono", - "size": 12.0, + "size": 12, "weight": "normal" }, "padding": "8, 8, 8, 8", @@ -175,7 +175,7 @@ namespace SettingsModelUnitTests "backgroundImage": "made_you_look.jpeg", "backgroundImageStretchMode": "uniformToFill", "backgroundImageAlignment": "center", - "backgroundImageOpacity": 1.0, + "backgroundImageOpacity": 1, "scrollbarState": "visible", "snapOnInput": true, @@ -298,8 +298,8 @@ namespace SettingsModelUnitTests // complex command with key chords static constexpr std::string_view actionsString4A{ R"([ - { "command": { "action": "adjustFontSize", "delta": 1.0 }, "keys": "ctrl+c" }, - { "command": { "action": "adjustFontSize", "delta": 1.0 }, "keys": "ctrl+d" } + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+c" }, + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+d" } ])" }; // command with name and icon and multiple key chords @@ -323,8 +323,8 @@ namespace SettingsModelUnitTests { "name": "Change font size...", "commands": [ - { "command": { "action": "adjustFontSize", "delta": 1.0 } }, - { "command": { "action": "adjustFontSize", "delta": -1.0 } }, + { "command": { "action": "adjustFontSize", "delta": 1 } }, + { "command": { "action": "adjustFontSize", "delta": -1 } }, { "command": "resetFontSize" }, ] } @@ -523,7 +523,7 @@ namespace SettingsModelUnitTests "name": "Profile with legacy font settings", "fontFace": "Cascadia Mono", - "fontSize": 12.0, + "fontSize": 12, "fontWeight": "normal" })" }; @@ -533,7 +533,7 @@ namespace SettingsModelUnitTests "font": { "face": "Cascadia Mono", - "size": 12.0, + "size": 12, "weight": "normal" } })" }; @@ -1007,8 +1007,8 @@ namespace SettingsModelUnitTests { "name": "Change font size...", "commands": [ - { "command": { "action": "adjustFontSize", "delta": 1.0 } }, - { "command": { "action": "adjustFontSize", "delta": -1.0 } }, + { "command": { "action": "adjustFontSize", "delta": 1 } }, + { "command": { "action": "adjustFontSize", "delta": -1 } }, { "command": "resetFontSize" }, ] } diff --git a/src/cascadia/ut_app/JsonUtilsTests.cpp b/src/cascadia/ut_app/JsonUtilsTests.cpp index 475e76abb5..29404fc1a9 100644 --- a/src/cascadia/ut_app/JsonUtilsTests.cpp +++ b/src/cascadia/ut_app/JsonUtilsTests.cpp @@ -354,16 +354,16 @@ namespace TerminalAppUnitTests TryBasicType(int{ -1024 }, -1024); TryBasicType(std::numeric_limits::max(), std::numeric_limits::max()); TryBasicType(false, false); - TryBasicType(1.0f, 1.0f); + TryBasicType(1.1f, 1.1f); // string -> wstring TryBasicType(std::wstring{ L"hello" }, "hello"); // float -> double - TryBasicType(1.0, 1.0f); + TryBasicType(static_cast(1.1f), 1.1f); // double -> float - TryBasicType(1.0f, 1.0); + TryBasicType(1.1f, static_cast(1.1f)); TryBasicType(til::color{ 0xab, 0xcd, 0xef }, "#ABCDEF"); TryBasicType(til::color{ 0xcc, 0xcc, 0xcc }, "#CCC", "#CCCCCC"); diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 674dc3baf7..2fbaf7cd52 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -493,7 +493,7 @@ void AtlasEngine::SetWarningCallback(std::function& features, const std::unordered_map& axes) noexcept +[[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept { // We're currently faced with a font caching bug that we're unable to reproduce locally. See GH#9375. // But it occurs often enough and has no proper workarounds, so we're forced to fix it. @@ -544,7 +544,7 @@ void AtlasEngine::_resolveTransparencySettings() noexcept } } -[[nodiscard]] HRESULT AtlasEngine::_updateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept +[[nodiscard]] HRESULT AtlasEngine::_updateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept try { std::vector fontFeatures; @@ -567,19 +567,20 @@ try if (p.first.size() == 4) { const auto s = p.first.data(); + const auto v = static_cast(std::max(0l, lrintf(p.second))); switch (const auto tag = DWRITE_MAKE_FONT_FEATURE_TAG(s[0], s[1], s[2], s[3])) { case DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES: - fontFeatures[0].parameter = p.second; + fontFeatures[0].parameter = v; break; case DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES: - fontFeatures[1].parameter = p.second; + fontFeatures[1].parameter = v; break; case DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES: - fontFeatures[2].parameter = p.second; + fontFeatures[2].parameter = v; break; default: - fontFeatures.emplace_back(tag, p.second); + fontFeatures.emplace_back(tag, v); break; } } diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 9ad4425bea..1f26644ebe 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -78,7 +78,7 @@ namespace Microsoft::Console::Render::Atlas void SetGraphicsAPI(GraphicsAPI graphicsAPI) noexcept; void SetWarningCallback(std::function pfn) noexcept; [[nodiscard]] HRESULT SetWindowSize(til::size pixels) noexcept; - [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; private: // AtlasEngine.cpp @@ -96,7 +96,7 @@ namespace Microsoft::Console::Render::Atlas // AtlasEngine.api.cpp void _resolveTransparencySettings() noexcept; - [[nodiscard]] HRESULT _updateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; + [[nodiscard]] HRESULT _updateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; void _resolveFontMetrics(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics = nullptr); [[nodiscard]] bool _updateWithNearbyFontCollection() noexcept; From 2f52f27197731a91d202fe101239091338860427 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 1 May 2024 15:17:49 -0500 Subject: [PATCH 34/53] build: switch to ESRP v5, which supports managed identities (#17134) This required me to push a bunch more parameters through the build pipeline, but it gave me the opportunity to define them as variables that can be set at queue time. --- .github/actions/spelling/allow/microsoft.txt | 2 ++ build/pipelines/ob-nightly.yml | 7 +++++++ build/pipelines/ob-release.yml | 7 +++++++ .../pipelines/templates-v2/job-build-package-wpf.yml | 12 ++++++++++-- build/pipelines/templates-v2/job-build-project.yml | 12 ++++++++++-- .../templates-v2/job-merge-msix-into-bundle.yml | 12 ++++++++++-- build/pipelines/templates-v2/job-package-conpty.yml | 12 ++++++++++-- .../templates-v2/pipeline-full-release-build.yml | 2 +- .../pipeline-onebranch-full-release-build.yml | 8 ++++++++ 9 files changed, 65 insertions(+), 9 deletions(-) diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 876d9fd960..940b0fc29d 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -1,6 +1,8 @@ ACLs ADMINS advapi +akv +AKV altform altforms appendwttlogging diff --git a/build/pipelines/ob-nightly.yml b/build/pipelines/ob-nightly.yml index 99cff5b206..18a1a1ddec 100644 --- a/build/pipelines/ob-nightly.yml +++ b/build/pipelines/ob-nightly.yml @@ -30,6 +30,13 @@ extends: buildTerminal: true pgoBuildMode: Optimize codeSign: true + signingIdentity: + serviceName: $(SigningServiceName) + appId: $(SigningAppId) + tenantId: $(SigningTenantId) + akvName: $(SigningAKVName) + authCertName: $(SigningAuthCertName) + signCertName: $(SigningSignCertName) publishSymbolsToPublic: true publishVpackToWindows: false symbolExpiryTime: 15 diff --git a/build/pipelines/ob-release.yml b/build/pipelines/ob-release.yml index 147fef4e5a..7ae0e71d06 100644 --- a/build/pipelines/ob-release.yml +++ b/build/pipelines/ob-release.yml @@ -78,6 +78,13 @@ extends: buildConfigurations: ${{ parameters.buildConfigurations }} buildPlatforms: ${{ parameters.buildPlatforms }} codeSign: true + signingIdentity: + serviceName: $(SigningServiceName) + appId: $(SigningAppId) + tenantId: $(SigningTenantId) + akvName: $(SigningAKVName) + authCertName: $(SigningAuthCertName) + signCertName: $(SigningSignCertName) terminalInternalPackageVersion: ${{ parameters.terminalInternalPackageVersion }} publishSymbolsToPublic: ${{ parameters.publishSymbolsToPublic }} publishVpackToWindows: ${{ parameters.publishVpackToWindows }} diff --git a/build/pipelines/templates-v2/job-build-package-wpf.yml b/build/pipelines/templates-v2/job-build-package-wpf.yml index 9d64e1c7cf..a9656a65e6 100644 --- a/build/pipelines/templates-v2/job-build-package-wpf.yml +++ b/build/pipelines/templates-v2/job-build-package-wpf.yml @@ -27,6 +27,9 @@ parameters: - name: publishArtifacts type: boolean default: true + - name: signingIdentity + type: object + default: {} jobs: - job: ${{ parameters.jobName }} @@ -97,10 +100,15 @@ jobs: flattenFolders: true - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@3 + - task: EsrpCodeSigning@5 displayName: Submit *.nupkg to ESRP for code signing inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} + AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} + AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} + AuthAKVName: ${{ parameters.signingIdentity.akvName }} + AuthCertName: ${{ parameters.signingIdentity.authCertName }} + AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} FolderPath: $(Build.ArtifactStagingDirectory)/nupkg Pattern: '*.nupkg' UseMinimatch: true diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml index d3c10bb822..ed3cd0844c 100644 --- a/build/pipelines/templates-v2/job-build-project.yml +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -65,6 +65,9 @@ parameters: - name: removeAllNonSignedFiles type: boolean default: false + - name: signingIdentity + type: object + default: {} jobs: - job: ${{ parameters.jobName }} @@ -239,10 +242,15 @@ jobs: # Code-sign everything we just put together. # We run the signing in Terminal.BinDir, because all of the signing batches are relative to the final architecture/configuration output folder. - - task: EsrpCodeSigning@3 + - task: EsrpCodeSigning@5 displayName: Submit Signing Request inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} + AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} + AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} + AuthAKVName: ${{ parameters.signingIdentity.akvName }} + AuthCertName: ${{ parameters.signingIdentity.authCertName }} + AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} FolderPath: '$(Terminal.BinDir)' signType: batchSigning batchSignPolicyFile: '$(Build.SourcesDirectory)/ESRPSigningConfig.json' diff --git a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml index 6d6ad09cda..e3644e35bc 100644 --- a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml +++ b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml @@ -32,6 +32,9 @@ parameters: - name: afterBuildSteps type: stepList default: [] + - name: signingIdentity + type: object + default: {} jobs: - job: ${{ parameters.jobName }} @@ -94,10 +97,15 @@ jobs: displayName: Create msixbundle - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@3 + - task: EsrpCodeSigning@5 displayName: Submit *.msixbundle to ESRP for code signing inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} + AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} + AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} + AuthAKVName: ${{ parameters.signingIdentity.akvName }} + AuthCertName: ${{ parameters.signingIdentity.authCertName }} + AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} FolderPath: $(System.ArtifactsDirectory)\bundle Pattern: $(BundleStemName)*.msixbundle UseMinimatch: true diff --git a/build/pipelines/templates-v2/job-package-conpty.yml b/build/pipelines/templates-v2/job-package-conpty.yml index 2f777cdf4f..e09775f836 100644 --- a/build/pipelines/templates-v2/job-package-conpty.yml +++ b/build/pipelines/templates-v2/job-package-conpty.yml @@ -27,6 +27,9 @@ parameters: - name: publishArtifacts type: boolean default: true + - name: signingIdentity + type: object + default: {} jobs: - job: ${{ parameters.jobName }} @@ -82,10 +85,15 @@ jobs: versionEnvVar: XES_PACKAGEVERSIONNUMBER - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@3 + - task: EsrpCodeSigning@5 displayName: Submit *.nupkg to ESRP for code signing inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} + AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} + AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} + AuthAKVName: ${{ parameters.signingIdentity.akvName }} + AuthCertName: ${{ parameters.signingIdentity.authCertName }} + AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} FolderPath: $(Build.ArtifactStagingDirectory)/nupkg Pattern: '*.nupkg' UseMinimatch: true diff --git a/build/pipelines/templates-v2/pipeline-full-release-build.yml b/build/pipelines/templates-v2/pipeline-full-release-build.yml index 92800efe18..d2a32c8c46 100644 --- a/build/pipelines/templates-v2/pipeline-full-release-build.yml +++ b/build/pipelines/templates-v2/pipeline-full-release-build.yml @@ -33,7 +33,7 @@ parameters: - arm64 - name: codeSign type: boolean - default: true + default: false - name: generateSbom type: boolean default: true diff --git a/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml index 9fc9df3068..41a2dc0002 100644 --- a/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml +++ b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml @@ -60,6 +60,9 @@ parameters: - name: extraPublishJobs type: object default: [] + - name: signingIdentity + type: object + default: {} resources: repositories: @@ -125,6 +128,7 @@ extends: generateSbom: false # this is handled by onebranch removeAllNonSignedFiles: true # appease the overlords codeSign: ${{ parameters.codeSign }} + signingIdentity: ${{ parameters.signingIdentity }} beforeBuildSteps: # Right before we build, lay down the universal package and localizations - task: PkgESSetupBuild@12 displayName: Package ES - Setup Build @@ -161,6 +165,7 @@ extends: generateSbom: false # this is handled by onebranch removeAllNonSignedFiles: true # appease the overlords codeSign: ${{ parameters.codeSign }} + signingIdentity: ${{ parameters.signingIdentity }} beforeBuildSteps: - task: PkgESSetupBuild@12 displayName: Package ES - Setup Build @@ -214,6 +219,7 @@ extends: buildPlatforms: ${{ parameters.buildPlatforms }} generateSbom: false # Handled by onebranch codeSign: ${{ parameters.codeSign }} + signingIdentity: ${{ parameters.signingIdentity }} afterBuildSteps: # This directory has to exist, even if we aren't using createvpack, because the Guardian rules demand it. - pwsh: |- @@ -241,6 +247,7 @@ extends: buildPlatforms: ${{ parameters.buildPlatforms }} generateSbom: false # this is handled by onebranch codeSign: ${{ parameters.codeSign }} + signingIdentity: ${{ parameters.signingIdentity }} - ${{ if eq(parameters.buildWPF, true) }}: - template: ./build/pipelines/templates-v2/job-build-package-wpf.yml@self @@ -258,6 +265,7 @@ extends: buildPlatforms: ${{ parameters.buildPlatforms }} generateSbom: false # this is handled by onebranch codeSign: ${{ parameters.codeSign }} + signingIdentity: ${{ parameters.signingIdentity }} - stage: Publish displayName: Publish From a5835b01b1607bfac1141f0cd4ce9839f260a91f Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 1 May 2024 21:11:25 -0500 Subject: [PATCH 35/53] Prevent multiple settings tabs from being persisted (#17169) Re-add some machinery to special case settings tabs. When we're going to persist a tab that only has a single settings pane in it, we'll now promote that to the first-class "openSettings" action. This will allow our other code in TerminalPage to route multiple settings tabs all to the same tab. This of course doesn't stop you from opening multiple settings tabs with `{ "command": {"action": "newTab", "type": "settings"} }` actions. If we did that, then that would prevent someone from having a settings pane in the first pane, and a terminal to the right. Closes #17070 --- src/cascadia/TerminalApp/TerminalTab.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index e0a93ee927..d3e52d4037 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -449,9 +449,23 @@ namespace winrt::TerminalApp::implementation { ActionAndArgs newTabAction{}; + INewContentArgs newContentArgs{ state.firstPane->GetTerminalArgsForPane(kind) }; + + // Special case here: if there was one pane (which results in no actions + // being generated), and it was a settings pane, then promote that to an + // open settings action. The openSettings action itself has additional machinery + // to prevent multiple top-level settings tabs. + const auto wasSettings = state.args.empty() && + (newContentArgs && newContentArgs.Type() == L"settings"); + if (wasSettings) + { + newTabAction.Action(ShortcutAction::OpenSettings); + newTabAction.Args(OpenSettingsArgs{ SettingsTarget::SettingsUI }); + return std::vector{ std::move(newTabAction) }; + } + newTabAction.Action(ShortcutAction::NewTab); - NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane(kind) }; - newTabAction.Args(newTabArgs); + newTabAction.Args(NewTabArgs{ newContentArgs }); state.args.emplace(state.args.begin(), std::move(newTabAction)); } From 015055c246b6c2a55128e9961b7e4cfc81ad0a62 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 2 May 2024 11:12:06 -0500 Subject: [PATCH 36/53] Move the AttachConsole in the feature tests in the retry loop (#17180) These feature tests continue to plague us. Seems like the most likely outcome nowadays is that the test fails to attach immediately, so we don't even get to the retry loop. Easy enough. Let's move the AttachConsole into the loop too. --- src/host/ft_host/InitTests.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/host/ft_host/InitTests.cpp b/src/host/ft_host/InitTests.cpp index 50e5c0cf47..2612ce8e6b 100644 --- a/src/host/ft_host/InitTests.cpp +++ b/src/host/ft_host/InitTests.cpp @@ -237,13 +237,23 @@ MODULE_SETUP(ModuleSetup) // to the one that belongs to the CMD.exe in the new OpenConsole.exe window. VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FreeConsole()); - VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(AttachConsole(dwFindPid)); + BOOL attached = FALSE; int tries = 0; DWORD delay; // This will wait for up to 32s in total (from 10ms to 163840ms) for (delay = 10; delay < 30000u; delay *= 2) { + if (!attached) + { + attached = AttachConsole(dwFindPid); + if (!attached) + { + WaitForSingleObject(GetCurrentThread(), delay); + continue; + } + } + tries++; Log::Comment(NoThrowString().Format(L"Attempt #%d to confirm we've attached", tries)); @@ -281,6 +291,7 @@ MODULE_SETUP(ModuleSetup) } }; + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(attached, L"Make sure successfully attached to the console"); VERIFY_IS_LESS_THAN(delay, 30000u, L"Make sure we set up the new console in time"); return true; From c2b8f995820cd8191166b0a1b2bd7b41611e6c61 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 2 May 2024 11:12:19 -0500 Subject: [PATCH 37/53] Don't always focus pane content on Tapped, if the pane is already focused (#17174) You'll never believe this. Clicking on the dropdown button on a ComboBox doesn't set `e.Tapped = true`. It bubbles up, and lands in our `Pane`'s `Border`'s tapped handler. And in there, we yeet focus to the first content. We end up stealing focus from the combobox, and then the combobox doesn't actually open its dropdown. So yea we can just fix that. Easy enough. Closes #17062 --------- Co-authored-by: Dustin L. Howett Co-authored-by: Leonard Hecker --- .../ScratchIslandApp/SampleApp/MySettings.h | 2 +- src/cascadia/TerminalApp/Pane.cpp | 34 ++++++++++--------- src/cascadia/TerminalApp/Pane.h | 2 ++ .../GlobalAppearance.xaml | 3 +- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/scratch/ScratchIslandApp/SampleApp/MySettings.h b/scratch/ScratchIslandApp/SampleApp/MySettings.h index 3e490416d9..d73290390e 100644 --- a/scratch/ScratchIslandApp/SampleApp/MySettings.h +++ b/scratch/ScratchIslandApp/SampleApp/MySettings.h @@ -11,7 +11,7 @@ Licensed under the MIT license. #include #include "MySettings.g.h" -using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; namespace winrt::SampleApp::implementation diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index ac59961235..fd195d8d42 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -47,14 +47,8 @@ Pane::Pane(const IPaneContent& content, const bool lastFocused) : // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to // Colors::Transparent! The border won't get Tapped events, and they'll fall // through to something else. - _borderFirst.Tapped([this](auto&, auto& e) { - _FocusFirstChild(); - e.Handled(true); - }); - _borderSecond.Tapped([this](auto&, auto& e) { - _FocusFirstChild(); - e.Handled(true); - }); + _borderFirst.Tapped({ this, &Pane::_borderTappedHandler }); + _borderSecond.Tapped({ this, &Pane::_borderTappedHandler }); } Pane::Pane(std::shared_ptr first, @@ -88,14 +82,8 @@ Pane::Pane(std::shared_ptr first, // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to // Colors::Transparent! The border won't get Tapped events, and they'll fall // through to something else. - _borderFirst.Tapped([this](auto&, auto& e) { - _FocusFirstChild(); - e.Handled(true); - }); - _borderSecond.Tapped([this](auto&, auto& e) { - _FocusFirstChild(); - e.Handled(true); - }); + _borderFirst.Tapped({ this, &Pane::_borderTappedHandler }); + _borderSecond.Tapped({ this, &Pane::_borderTappedHandler }); } // Extract the terminal settings from the current (leaf) pane's control @@ -1237,6 +1225,14 @@ void Pane::UpdateVisuals() // - void Pane::_Focus() { + // Don't focus our content if we're already focused. This prevents a bug + // where tapping on the arrow in a ComboBox will land in our Tapped handler, + // and if we steal focus from the ComboBox, it won't open. See GH#17062 + if (WasLastFocused()) + { + return; + } + GotFocus.raise(shared_from_this(), FocusState::Programmatic); if (const auto& lastContent{ GetLastFocusedContent() }) { @@ -3021,3 +3017,9 @@ winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::_ComputeBorderColor() return _themeResources.unfocusedBorderBrush; } + +void Pane::_borderTappedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::Input::TappedRoutedEventArgs& e) +{ + _FocusFirstChild(); + e.Handled(true); +} diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 67daee1694..f25a7b0834 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -314,6 +314,8 @@ private: SplitState _convertAutomaticOrDirectionalSplitState(const winrt::Microsoft::Terminal::Settings::Model::SplitDirection& splitType) const; + void _borderTappedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Input::TappedRoutedEventArgs& e); + // Function Description: // - Returns true if the given direction can be used with the given split // type. diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml index 510779c27a..26bb23a414 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml @@ -31,7 +31,8 @@ + SelectedItem="{x:Bind ViewModel.CurrentLanguage, Mode=TwoWay}" + Style="{StaticResource ComboBoxSettingStyle}"> From 475b3878f675431b47aa7c11b8af403aa200b2b2 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 2 May 2024 18:12:34 +0200 Subject: [PATCH 38/53] Fix font axis/feature SUI issues (#17173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Something something code changes, fixes issue. Events need throwing, some events don't need throwing, some events need throttling to not overwhelm the flurble function within the gumbies component. This is all boilerplate and it works now. Closes #17171 Closes #17172 ## Validation Steps Performed * Resetting the font axis/feature expander keeps the add new button flyout sorted ✅ * Changing a font axis/feature value updates the preview ✅ * After changing the font, features/axes can still be edited ✅ --- .github/actions/spelling/expect/expect.txt | 5 + .../TerminalSettingsEditor/Appearances.cpp | 163 ++++++++++++------ .../TerminalSettingsEditor/Appearances.h | 20 +-- .../TerminalSettingsEditor/Appearances.xaml | 4 +- .../Profiles_Appearance.cpp | 24 ++- .../Profiles_Appearance.h | 10 +- 6 files changed, 145 insertions(+), 81 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 775694924d..5c0c14b0df 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -245,6 +245,7 @@ CONKBD conlibk conmsgl CONNECTINFO +connyection CONOUT conprops conpropsp @@ -1420,6 +1421,8 @@ PUCHAR pvar pwch PWDDMCONSOLECONTEXT +Pwease +pweview pws pwstr pwsz @@ -1898,6 +1901,7 @@ UVWXY UVWXYZ uwa uwp +uwu uxtheme Vanara vararg @@ -1971,6 +1975,7 @@ wdm webpage websites wekyb +wewoad wex wextest wextestclass diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.cpp b/src/cascadia/TerminalSettingsEditor/Appearances.cpp index c2823b955b..2ed71afc5a 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.cpp +++ b/src/cascadia/TerminalSettingsEditor/Appearances.cpp @@ -544,14 +544,33 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation return item; } + // Call this when all the _fontFaceDependents members have changed. void AppearanceViewModel::_notifyChangesForFontSettings() { - _NotifyChanges( - L"FontFaceDependents", - L"FontAxes", - L"FontFeatures", - L"HasFontAxes", - L"HasFontFeatures"); + _NotifyChanges(L"FontFaceDependents"); + _NotifyChanges(L"FontAxes"); + _NotifyChanges(L"FontFeatures"); + _NotifyChanges(L"HasFontAxes"); + _NotifyChanges(L"HasFontFeatures"); + } + + // Call this when used items moved into unused and vice versa. + // Because this doesn't recreate the IObservableVector instances, + // we don't need to notify the UI about changes to the "FontAxes" property. + void AppearanceViewModel::_notifyChangesForFontSettingsReactive(FontSettingIndex fontSettingsIndex) + { + _NotifyChanges(L"FontFaceDependents"); + switch (fontSettingsIndex) + { + case FontAxesIndex: + _NotifyChanges(L"HasFontAxes"); + break; + case FontFeaturesIndex: + _NotifyChanges(L"HasFontFeatures"); + break; + default: + break; + } } double AppearanceViewModel::LineHeight() const @@ -616,6 +635,30 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation FontWeight(winrt::Microsoft::Terminal::UI::Converters::DoubleToFontWeight(fontWeight)); } + const AppearanceViewModel::FontFaceDependentsData& AppearanceViewModel::FontFaceDependents() + { + if (!_fontFaceDependents) + { + _refreshFontFaceDependents(); + } + return *_fontFaceDependents; + } + + winrt::hstring AppearanceViewModel::MissingFontFaces() + { + return FontFaceDependents().missingFontFaces; + } + + winrt::hstring AppearanceViewModel::ProportionalFontFaces() + { + return FontFaceDependents().proportionalFontFaces; + } + + bool AppearanceViewModel::HasPowerlineCharacters() + { + return FontFaceDependents().hasPowerlineCharacters; + } + IObservableVector AppearanceViewModel::FontAxes() { return FontFaceDependents().fontSettingsUsed[FontAxesIndex]; @@ -628,7 +671,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void AppearanceViewModel::ClearFontAxes() { - _deleteAllFontSettings(FontAxesIndex); + _deleteAllFontKeyValuePairs(FontAxesIndex); } Model::FontConfig AppearanceViewModel::FontAxesOverrideSource() const @@ -648,7 +691,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void AppearanceViewModel::ClearFontFeatures() { - _deleteAllFontSettings(FontFeaturesIndex); + _deleteAllFontKeyValuePairs(FontFeaturesIndex); } Model::FontConfig AppearanceViewModel::FontFeaturesOverrideSource() const @@ -664,7 +707,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } const auto kvImpl = winrt::get_self(kv); - const auto fontSettingsIndex = kvImpl->IsFontFeature() ? 1 : 0; + const auto fontSettingsIndex = kvImpl->IsFontFeature() ? FontFeaturesIndex : FontAxesIndex; auto& d = *_fontFaceDependents; auto& used = d.fontSettingsUsed[fontSettingsIndex]; auto& unused = d.fontSettingsUnused[fontSettingsIndex]; @@ -686,7 +729,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation unused.erase(it); - _notifyChangesForFontSettings(); + _notifyChangesForFontSettingsReactive(fontSettingsIndex); } void AppearanceViewModel::DeleteFontKeyValuePair(const Editor::FontKeyValuePair& kv) @@ -699,10 +742,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto kvImpl = winrt::get_self(kv); const auto tag = kvImpl->Key(); const auto tagString = tagToString(tag); - const auto fontSettingsIndex = kvImpl->IsFontFeature() ? 1 : 0; + const auto fontSettingsIndex = kvImpl->IsFontFeature() ? FontFeaturesIndex : FontAxesIndex; auto& d = *_fontFaceDependents; auto& used = d.fontSettingsUsed[fontSettingsIndex]; - auto& unused = d.fontSettingsUnused[fontSettingsIndex]; const auto fontInfo = _appearance.SourceProfile().FontInfo(); auto fontSettingsUser = kvImpl->IsFontFeature() ? fontInfo.FontFeatures() : fontInfo.FontAxes(); @@ -719,20 +761,59 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation fontSettingsUser.Remove(std::wstring_view{ tagString }); - // Insert the item into the unused list, keeping it sorted by the display text. - { - const auto item = _createFontSettingMenuItem(*it); - const auto it = std::lower_bound(unused.begin(), unused.end(), item, [](const MenuFlyoutItemBase& lhs, const MenuFlyoutItemBase& rhs) { - const auto& a = lhs.as().Text(); - const auto& b = rhs.as().Text(); - return til::compare_linguistic_insensitive(a, b) < 0; - }); - unused.insert(it, item); - } - + _addMenuFlyoutItemToUnused(fontSettingsIndex, _createFontSettingMenuItem(*it)); used.RemoveAt(gsl::narrow(it - used.begin())); - _notifyChangesForFontSettings(); + _notifyChangesForFontSettingsReactive(fontSettingsIndex); + } + + void AppearanceViewModel::_deleteAllFontKeyValuePairs(FontSettingIndex fontSettingsIndex) + { + const auto fontInfo = _appearance.SourceProfile().FontInfo(); + if (fontSettingsIndex == FontFeaturesIndex) + { + fontInfo.ClearFontFeatures(); + } + else + { + fontInfo.ClearFontAxes(); + } + + if (!_fontFaceDependents) + { + return; + } + + auto& d = *_fontFaceDependents; + auto& used = d.fontSettingsUsed[fontSettingsIndex]; + + for (const auto& kv : used) + { + _addMenuFlyoutItemToUnused(fontSettingsIndex, _createFontSettingMenuItem(kv)); + } + + used.Clear(); + + _notifyChangesForFontSettingsReactive(fontSettingsIndex); + } + + // Inserts the given menu item into the unused list, while keeping it sorted by the display text. + void AppearanceViewModel::_addMenuFlyoutItemToUnused(FontSettingIndex index, MenuFlyoutItemBase item) + { + if (!_fontFaceDependents) + { + return; + } + + auto& d = *_fontFaceDependents; + auto& unused = d.fontSettingsUnused[index]; + + const auto it = std::lower_bound(unused.begin(), unused.end(), item, [](const MenuFlyoutItemBase& lhs, const MenuFlyoutItemBase& rhs) { + const auto& a = lhs.as().Text(); + const auto& b = rhs.as().Text(); + return til::compare_linguistic_insensitive(a, b) < 0; + }); + unused.insert(it, std::move(item)); } void AppearanceViewModel::UpdateFontSetting(const FontKeyValuePair* kvImpl) @@ -757,37 +838,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } std::ignore = fontSettingsUser.Insert(std::wstring_view{ tagString }, value); - } - - void AppearanceViewModel::_deleteAllFontSettings(FontSettingIndex fontSettingsIndex) - { - const auto fontInfo = _appearance.SourceProfile().FontInfo(); - if (fontSettingsIndex == FontFeaturesIndex) - { - fontInfo.ClearFontFeatures(); - } - else - { - fontInfo.ClearFontAxes(); - } - - if (!_fontFaceDependents) - { - return; - } - - auto& d = *_fontFaceDependents; - auto& used = d.fontSettingsUsed[fontSettingsIndex]; - auto& unused = d.fontSettingsUnused[fontSettingsIndex]; - - for (const auto& kv : used) - { - unused.emplace_back(_createFontSettingMenuItem(kv)); - } - - used.Clear(); - - _notifyChangesForFontSettings(); + // Pwease call Profiles_Appearance::_onProfilePropertyChanged to make the pweview connyection wewoad. Thanks!! uwu + // ...I hate this. + _NotifyChanges(L"uwu"); } void AppearanceViewModel::SetBackgroundImageOpacityFromPercentageValue(double percentageValue) diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.h b/src/cascadia/TerminalSettingsEditor/Appearances.h index 572b624098..7ad8a03ce4 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.h +++ b/src/cascadia/TerminalSettingsEditor/Appearances.h @@ -94,18 +94,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void SetFontWeightFromDouble(double fontWeight); - const FontFaceDependentsData& FontFaceDependents() - { - if (!_fontFaceDependents) - { - _refreshFontFaceDependents(); - } - return *_fontFaceDependents; - } - - winrt::hstring MissingFontFaces() { return FontFaceDependents().missingFontFaces; } - winrt::hstring ProportionalFontFaces() { return FontFaceDependents().proportionalFontFaces; } - bool HasPowerlineCharacters() { return FontFaceDependents().hasPowerlineCharacters; } + const FontFaceDependentsData& FontFaceDependents(); + winrt::hstring MissingFontFaces(); + winrt::hstring ProportionalFontFaces(); + bool HasPowerlineCharacters(); Windows::Foundation::Collections::IObservableVector FontAxes(); bool HasFontAxes() const; @@ -165,7 +157,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void _generateFontFeatures(IDWriteFontFace* fontFace, std::vector& list); Windows::UI::Xaml::Controls::MenuFlyoutItemBase _createFontSettingMenuItem(const Editor::FontKeyValuePair& kv); void _notifyChangesForFontSettings(); - void _deleteAllFontSettings(FontSettingIndex index); + void _notifyChangesForFontSettingsReactive(FontSettingIndex fontSettingsIndex); + void _deleteAllFontKeyValuePairs(FontSettingIndex index); + void _addMenuFlyoutItemToUnused(FontSettingIndex index, Windows::UI::Xaml::Controls::MenuFlyoutItemBase item); Model::AppearanceConfig _appearance; winrt::hstring _lastBgImagePath; diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml index 2f94cf4473..a3f643c53c 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml +++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml @@ -309,7 +309,7 @@ @@ -336,7 +336,7 @@ diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp index 5295d3042b..cdf1e851ae 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp @@ -3,13 +3,11 @@ #include "pch.h" #include "Profiles_Appearance.h" -#include "Profiles_Appearance.g.cpp" + #include "ProfileViewModel.h" #include "PreviewConnection.h" -#include "EnumEntry.h" -#include -#include "..\WinRTUtils\inc\Utils.h" +#include "Profiles_Appearance.g.cpp" using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Navigation; @@ -64,10 +62,20 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _Profile.DeleteUnfocusedAppearance(); } - void Profiles_Appearance::_onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const + void Profiles_Appearance::_onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) { - const auto settings = _Profile.TermSettings(); - _previewConnection->DisplayPowerlineGlyphs(_Profile.DefaultAppearance().HasPowerlineCharacters()); - _previewControl.UpdateControlSettings(settings, settings); + if (!_updatePreviewControl) + { + _updatePreviewControl = std::make_shared>( + winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), + std::chrono::milliseconds{ 100 }, + [this]() { + const auto settings = _Profile.TermSettings(); + _previewConnection->DisplayPowerlineGlyphs(_Profile.DefaultAppearance().HasPowerlineCharacters()); + _previewControl.UpdateControlSettings(settings, settings); + }); + } + + _updatePreviewControl->Run(); } } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h index ab9ebf66fb..27c8e455a5 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h @@ -3,9 +3,12 @@ #pragma once -#include "Profiles_Appearance.g.h" -#include "Utils.h" +#include + #include "PreviewConnection.h" +#include "Utils.h" + +#include "Profiles_Appearance.g.h" namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { @@ -26,10 +29,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_PROPERTY(Editor::ProfileViewModel, Profile, nullptr); private: - void _onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const; + void _onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&); winrt::com_ptr _previewConnection{ nullptr }; Microsoft::Terminal::Control::TermControl _previewControl{ nullptr }; + std::shared_ptr> _updatePreviewControl; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _AppearanceViewModelChangedRevoker; Editor::IHostedInWindow _windowRoot; From a9446a12dfac58325c2f6cf02edc9cb23f685021 Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Thu, 2 May 2024 11:13:50 -0500 Subject: [PATCH 39/53] Localization Updates - main - 05/02/2024 03:05:18 (#17175) --- .../Resources/qps-ploc/Resources.resw | 6 ++++++ .../Resources/qps-ploca/Resources.resw | 6 ++++++ .../Resources/qps-plocm/Resources.resw | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw index 4d9d01daca..a9e220a076 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw @@ -733,10 +733,16 @@ Ьřоẅşé... !!! Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Άδď ňėώ ƒόņţ α×ϊŝ !!! !! + Λďð ňéẁ !! Button label that adds a new font axis for the current font. + + Аđδ ʼněω ƒóлт ƒ℮ãтцřе !!! !!! + Άðδ ⁿэщ !! Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw index 4d9d01daca..a9e220a076 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw @@ -733,10 +733,16 @@ Ьřоẅşé... !!! Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Άδď ňėώ ƒόņţ α×ϊŝ !!! !! + Λďð ňéẁ !! Button label that adds a new font axis for the current font. + + Аđδ ʼněω ƒóлт ƒ℮ãтцřе !!! !!! + Άðδ ⁿэщ !! Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw index 4d9d01daca..a9e220a076 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw @@ -733,10 +733,16 @@ Ьřоẅşé... !!! Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Άδď ňėώ ƒόņţ α×ϊŝ !!! !! + Λďð ňéẁ !! Button label that adds a new font axis for the current font. + + Аđδ ʼněω ƒóлт ƒ℮ãтцřе !!! !!! + Άðδ ⁿэщ !! Button label that adds a new font feature for the current font. From 6cda6797f8289cb727ba9fe95f4649a5eec417f0 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 2 May 2024 11:14:20 -0500 Subject: [PATCH 40/53] Take wrapping into account when expanding wordwise selections (#17170) Closes #17165 --- .github/actions/spelling/allow/allow.txt | 1 + src/buffer/out/textBuffer.cpp | 35 ++++++++-- src/host/ut_host/TextBufferTests.cpp | 85 +++++++++++++++--------- 3 files changed, 86 insertions(+), 35 deletions(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index f6c03606a5..d5f5f9c2a7 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -80,6 +80,7 @@ mnt mru nje noreply +notwrapped ogonek ok'd overlined diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 10c70646e9..2aa9176a8c 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1440,10 +1440,23 @@ til::point TextBuffer::_GetWordStartForSelection(const til::point target, const // expand left until we hit the left boundary or a different delimiter class while (result != bufferSize.Origin() && _GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter) { - //prevent selection wrapping on whitespace selection - if (isControlChar && result.x == bufferSize.Left()) + if (result.x == bufferSize.Left()) { - break; + // Prevent wrapping to the previous line if the selection begins on whitespace + if (isControlChar) + { + break; + } + + if (result.y > 0) + { + // Prevent wrapping to the previous line if it was hard-wrapped (e.g. not forced by us to wrap) + const auto& priorRow = GetRowByOffset(result.y - 1); + if (!priorRow.WasWrapForced()) + { + break; + } + } } bufferSize.DecrementInBounds(result); } @@ -1563,10 +1576,22 @@ til::point TextBuffer::_GetWordEndForSelection(const til::point target, const st // expand right until we hit the right boundary as a ControlChar or a different delimiter class while (result != bufferSize.BottomRightInclusive() && _GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter) { - if (isControlChar && result.x == bufferSize.RightInclusive()) + if (result.x == bufferSize.RightInclusive()) { - break; + // Prevent wrapping to the next line if the selection begins on whitespace + if (isControlChar) + { + break; + } + + // Prevent wrapping to the next line if this one was hard-wrapped (e.g. not forced by us to wrap) + const auto& row = GetRowByOffset(result.y); + if (!row.WasWrapForced()) + { + break; + } } + bufferSize.IncrementInBoundsCircular(result); } diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 8f837985b7..f70f5c6b8e 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -2336,16 +2336,32 @@ void TextBufferTests::GetWordBoundaries() } _buffer->Reset(); - _buffer->ResizeTraditional({ 10, 5 }); + _buffer->ResizeTraditional({ 10, 6 }); const std::vector secondText = { L"this wordiswrapped", + L"notwrapped" L"spaces wrapped reachEOB" }; - //Buffer looks like: - // this wordi - // swrapped - // spaces - // wrappe - // d reachEOB + WriteLinesToBuffer(secondText, *_buffer); + + //Buffer looks like: + // 0123456789 + // 0|this wordi| < wrapped + // 1|swrapped | < not wrapped + // 2|notwrapped| < not wrapped + // 3|spaces | < wrapped + // 4| wrappe| < wrapped + // 5|d reachEOB| < wrapped + + VERIFY_IS_TRUE(_buffer->GetRowByOffset(0).WasWrapForced()); + VERIFY_IS_FALSE(_buffer->GetRowByOffset(1).WasWrapForced()); + // GH#780 See the comment in WriteLinesToBuffer + // VERIFY_IS_FALSE(_buffer->GetRowByOffset(2).WasWrapForced()); + _buffer->GetMutableRowByOffset(2).SetWrapForced(false); // Ugh + VERIFY_IS_TRUE(_buffer->GetRowByOffset(3).WasWrapForced()); + VERIFY_IS_TRUE(_buffer->GetRowByOffset(4).WasWrapForced()); + VERIFY_IS_TRUE(_buffer->GetRowByOffset(5).WasWrapForced()); + + // clang-format off testData = { { { 0, 0 }, { { 0, 0 }, { 0, 0 } } }, { { 1, 0 }, { { 0, 0 }, { 0, 0 } } }, @@ -2358,15 +2374,18 @@ void TextBufferTests::GetWordBoundaries() { { 9, 1 }, { { 8, 1 }, { 5, 0 } } }, { { 0, 2 }, { { 0, 2 }, { 0, 2 } } }, - { { 7, 2 }, { { 6, 2 }, { 0, 2 } } }, + { { 9, 2 }, { { 0, 2 }, { 0, 2 } } }, + // v accessibility does not consider wrapping + { { 0, 3 }, { { 0, 3 }, { 0, 2 } } }, + { { 7, 3 }, { { 6, 3 }, { 0, 2 } } }, + // v accessibility does not consider wrapping + { { 1, 4 }, { { 0, 4 }, { 0, 2 } } }, + { { 4, 4 }, { { 4, 4 }, { 4, 4 } } }, + { { 8, 4 }, { { 4, 4 }, { 4, 4 } } }, - { { 1, 3 }, { { 0, 3 }, { 0, 2 } } }, - { { 4, 3 }, { { 4, 3 }, { 4, 3 } } }, - { { 8, 3 }, { { 4, 3 }, { 4, 3 } } }, - - { { 0, 4 }, { { 4, 3 }, { 4, 3 } } }, - { { 1, 4 }, { { 1, 4 }, { 4, 3 } } }, - { { 9, 4 }, { { 2, 4 }, { 2, 4 } } }, + { { 0, 5 }, { { 4, 4 }, { 4, 4 } } }, + { { 1, 5 }, { { 1, 5 }, { 4, 4 } } }, + { { 9, 5 }, { { 2, 5 }, { 2, 5 } } }, }; for (const auto& test : testData) { @@ -2377,12 +2396,15 @@ void TextBufferTests::GetWordBoundaries() } //GetWordEnd for Wrapping Text - //Buffer looks like: - // this wordi - // swrapped - // spaces - // wrappe - // d reachEOB + // Buffer: + // 0123456789 + // 0|this wordi| < wrapped + // 1|swrapped | < not wrapped + // 2|notwrapped| < not wrapped + // 3|spaces | < wrapped + // 4| wrappe| < wrapped + // 5|d reachEOB| < wrapped + // clang-format off testData = { // tests for first line of text { { 0, 0 }, { { 3, 0 }, { 5, 0 } } }, @@ -2395,17 +2417,20 @@ void TextBufferTests::GetWordBoundaries() { { 7, 1 }, { { 7, 1 }, { 0, 2 } } }, { { 9, 1 }, { { 9, 1 }, { 0, 2 } } }, - { { 0, 2 }, { { 5, 2 }, { 4, 3 } } }, - { { 7, 2 }, { { 9, 2 }, { 4, 3 } } }, + { { 0, 2 }, { { 9, 2 }, { 4, 4 } } }, + { { 9, 2 }, { { 9, 2 }, { 4, 4 } } }, - { { 1, 3 }, { { 3, 3 }, { 4, 3 } } }, - { { 4, 3 }, { { 0, 4 }, { 2, 4 } } }, - { { 8, 3 }, { { 0, 4 }, { 2, 4 } } }, + { { 0, 3 }, { { 5, 3 }, { 4, 4 } } }, + { { 7, 3 }, { { 9, 3 }, { 4, 4 } } }, - { { 0, 4 }, { { 0, 4 }, { 2, 4 } } }, - { { 1, 4 }, { { 1, 4 }, { 2, 4 } } }, - { { 4, 4 }, { { 9, 4 }, { 0, 5 } } }, - { { 9, 4 }, { { 9, 4 }, { 0, 5 } } }, + { { 1, 4 }, { { 3, 4 }, { 4, 4 } } }, + { { 4, 4 }, { { 0, 5 }, { 2, 5 } } }, + { { 8, 4 }, { { 0, 5 }, { 2, 5 } } }, + + { { 0, 5 }, { { 0, 5 }, { 2, 5 } } }, + { { 1, 5 }, { { 1, 5 }, { 2, 5 } } }, + { { 4, 5 }, { { 9, 5 }, { 0, 6 } } }, + { { 9, 5 }, { { 9, 5 }, { 0, 6 } } }, }; // clang-format on From 8dd4512067871b85800236fa40feedebbc877c97 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 2 May 2024 14:15:25 -0500 Subject: [PATCH 41/53] Some schema updates for 1.21 (#17183) Noticed all these while prepping for Build: * Promotes the stabilized features out of `experimental.` * fixes a bug where a nested command with a `name` would match to a `renameWindow` action, instead of a command. * Adds the tab theme icon style * fixes a bug where `ScrollToMarkAction` wasn't in the list of possible args, so they would incorrectly get flagged as `moveTab` * outright adds `experimental.rightClickContextMenu` which was missing (?) --- doc/cascadia/profiles.schema.json | 46 +++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index d354bf0bcc..e0c5888bb2 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1501,8 +1501,10 @@ "const": "multipleActions" }, "actions": { - "$ref": "#/$defs/ShortcutAction", "type": "array", + "items": { + "$ref": "#/$defs/ShortcutAction" + }, "minItems": 1, "description": "A list of other actions." } @@ -1891,6 +1893,14 @@ ], "type": "string" }, + "IconStyle": { + "enum": [ + "default", + "hidden", + "monochrome" + ], + "type": "string" + }, "ThemeColor": { "description": "A special kind of color for use in themes. Can be an #rrggbb color, #rrggbbaa color, or a special value. 'accent' is evaluated as the user's selected Accent color in the OS, and 'terminalBackground' will be evaluated as the background color of the active terminal pane.", "oneOf": [ @@ -1928,6 +1938,10 @@ "showCloseButton": { "description": "Controls the visibility of the close button on the tab", "$ref": "#/$defs/ShowCloseButton" + }, + "iconStyle": { + "description": "Controls the appearance of a tab's icon", + "$ref": "#/$defs/IconStyle" } } }, @@ -2065,6 +2079,9 @@ { "$ref": "#/$defs/SwitchToTabAction" }, + { + "$ref": "#/$defs/ScrollToMarkAction" + }, { "$ref": "#/$defs/MoveFocusAction" }, @@ -2215,7 +2232,15 @@ "commands": { "description": "List of commands to execute", "items": { - "$ref": "#/$defs/Keybinding/properties/command" + "type": "object", + "properties": { + "command": { + "$ref": "#/$defs/Keybinding/properties/command" + }, + "name": { + "$ref": "#/$defs/Keybinding/properties/name" + } + } }, "minItems": 1, "type": "array" @@ -2776,20 +2801,35 @@ "type": "string" } }, - "experimental.autoMarkPrompts": { + "autoMarkPrompts": { "default": false, "description": "When set to true, prompts will automatically be marked.", "type": "boolean" }, + "experimental.autoMarkPrompts": { + "deprecated": true, + "description": "This has been replaced by autoMarkPrompts in 1.21", + "type": "boolean" + }, "experimental.retroTerminalEffect": { "description": "When set to true, enable retro terminal effects. This is an experimental feature, and its continued existence is not guaranteed.", "type": "boolean" }, "experimental.showMarksOnScrollbar": { + "deprecated": true, + "description": "This has been replaced by showMarksOnScrollbar in 1.21", + "type": "boolean" + }, + "showMarksOnScrollbar": { "default": false, "description": "When set to true, marks added to the buffer via the addMark action will appear on the scrollbar.", "type": "boolean" }, + "experimental.rightClickContextMenu": { + "default": false, + "description": "When set to true, right-clicking on the terminal will show a context menu. When set to false, right-click will copy", + "type": "boolean" + }, "experimental.repositionCursorWithMouse": { "default": false, "description": "When set to true, you can move the text cursor by clicking with the mouse on the current commandline. This is an experimental feature - there are lots of edge cases where this will not work as expected.", From c52dca40d27197a24bcbae9a48baf83a61429484 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 2 May 2024 21:38:04 +0200 Subject: [PATCH 42/53] Fix doskey macros for inputs with >1 consecutive whitespace (#17129) Initially the PR restored the original v1 conhost code for `MatchAndCopyAlias` which fixed the linked issue. Afterwards, I've taken the liberty to rewrite the code to use modern constructs again, primarily `string_view`. Additionally, the v1 code first counted the number of arguments and then iterated through it again to assemble them. This new code does both things at once. Closes #15736 ## Validation Steps Performed The unit tests have been extended to cover multiple consecutive spaces. All tests pass. --- src/host/alias.cpp | 443 ++++++++---------------------- src/host/alias.h | 39 +-- src/host/ut_host/AliasTests.cpp | 261 +----------------- src/inc/til.h | 4 +- src/renderer/atlas/BackendD3D.cpp | 2 +- 5 files changed, 127 insertions(+), 622 deletions(-) diff --git a/src/host/alias.cpp b/src/host/alias.cpp index a82fa19876..a658e2218d 100644 --- a/src/host/alias.cpp +++ b/src/host/alias.cpp @@ -21,7 +21,9 @@ using Microsoft::Console::Interactivity::ServiceLocator; struct case_insensitive_hash { - std::size_t operator()(const std::wstring& key) const + using is_transparent = void; + + std::size_t operator()(const std::wstring_view& key) const { til::hasher h; for (const auto& ch : key) @@ -34,9 +36,11 @@ struct case_insensitive_hash struct case_insensitive_equality { - bool operator()(const std::wstring& lhs, const std::wstring& rhs) const + using is_transparent = void; + + bool operator()(const std::wstring_view& lhs, const std::wstring_view& rhs) const { - return 0 == _wcsicmp(lhs.data(), rhs.data()); + return til::compare_ordinal_insensitive(lhs, rhs) == 0; } }; @@ -156,10 +160,10 @@ std::unordered_mapsecond; + const auto& exeData = exeIter->second; const auto sourceIter = exeData.find(sourceString); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_GEN_FAILURE), sourceIter == exeData.end()); - const auto targetString = sourceIter->second; + const auto& targetString = sourceIter->second; RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_GEN_FAILURE), targetString.size() == 0); // TargetLength is a byte count, convert to characters. @@ -333,7 +337,7 @@ static std::wstring aliasesSeparator(L"="); auto exeIter = g_aliasData.find(exeNameString); if (exeIter != g_aliasData.end()) { - auto list = exeIter->second; + const auto& list = exeIter->second; for (auto& pair : list) { // Alias stores lengths in bytes. @@ -464,7 +468,7 @@ void Alias::s_ClearCmdExeAliases() auto exeIter = g_aliasData.find(exeNameString); if (exeIter != g_aliasData.end()) { - auto list = exeIter->second; + const auto& list = exeIter->second; for (auto& pair : list) { // Alias stores lengths in bytes. @@ -817,292 +821,6 @@ void Alias::s_ClearCmdExeAliases() CATCH_RETURN(); } -// Routine Description: -// - Tokenizes a string into a collection using space as a separator -// Arguments: -// - str - String to tokenize -// Return Value: -// - Collection of tokenized strings -std::deque Alias::s_Tokenize(const std::wstring_view str) -{ - std::deque result; - - size_t prevIndex = 0; - auto spaceIndex = str.find(L' '); - while (std::wstring_view::npos != spaceIndex) - { - const auto length = spaceIndex - prevIndex; - - result.emplace_back(str.substr(prevIndex, length)); - - spaceIndex++; - prevIndex = spaceIndex; - - spaceIndex = str.find(L' ', spaceIndex); - } - - // Place the final one into the set. - result.emplace_back(str.substr(prevIndex)); - - return result; -} - -// Routine Description: -// - Gets just the arguments portion of the command string -// Specifically, all text after the first space character. -// Arguments: -// - str - String to split into just args -// Return Value: -// - Only the arguments part of the string or empty if there are no arguments. -std::wstring Alias::s_GetArgString(const std::wstring_view str) -{ - std::wstring result; - auto firstSpace = str.find_first_of(L' '); - if (std::wstring_view::npos != firstSpace) - { - firstSpace++; - if (firstSpace < str.size()) - { - result = str.substr(firstSpace); - } - } - - return result; -} - -// Routine Description: -// - Checks the given character to see if it is a numbered arg replacement macro -// and replaces it with the counted argument if there is a match -// Arguments: -// - ch - Character to test as a macro -// - appendToStr - Append the macro result here if it matched -// - tokens - Tokens of the original command string. 0 is alias. 1-N are arguments. -// Return Value: -// - True if we found the macro and appended to the string. -// - False if the given character doesn't match this macro. -bool Alias::s_TryReplaceNumberedArgMacro(const wchar_t ch, - std::wstring& appendToStr, - const std::deque& tokens) -{ - if (ch >= L'1' && ch <= L'9') - { - // Numerical macros substitute that numbered argument - const size_t index = ch - L'0'; - - if (index < tokens.size() && index > 0) - { - appendToStr.append(tokens[index]); - } - - return true; - } - - return false; -} - -// Routine Description: -// - Checks the given character to see if it is a wildcard arg replacement macro -// and replaces it with the entire argument string if there is a match -// Arguments: -// - ch - Character to test as a macro -// - appendToStr - Append the macro result here if it matched -// - fullArgString - All of the arguments as one big string. -// Return Value: -// - True if we found the macro and appended to the string. -// - False if the given character doesn't match this macro. -bool Alias::s_TryReplaceWildcardArgMacro(const wchar_t ch, - std::wstring& appendToStr, - const std::wstring fullArgString) -{ - if (L'*' == ch) - { - // Wildcard substitutes all arguments - appendToStr.append(fullArgString); - return true; - } - - return false; -} - -// Routine Description: -// - Checks the given character to see if it is an input redirection macro -// and replaces it with the < redirector if there is a match -// Arguments: -// - ch - Character to test as a macro -// - appendToStr - Append the macro result here if it matched -// Return Value: -// - True if we found the macro and appended to the string. -// - False if the given character doesn't match this macro. -bool Alias::s_TryReplaceInputRedirMacro(const wchar_t ch, - std::wstring& appendToStr) -{ - if (L'L' == towupper(ch)) - { - // L (either case) replaces with input redirector < - appendToStr.push_back(L'<'); - return true; - } - return false; -} - -// Routine Description: -// - Checks the given character to see if it is an output redirection macro -// and replaces it with the > redirector if there is a match -// Arguments: -// - ch - Character to test as a macro -// - appendToStr - Append the macro result here if it matched -// Return Value: -// - True if we found the macro and appended to the string. -// - False if the given character doesn't match this macro. -bool Alias::s_TryReplaceOutputRedirMacro(const wchar_t ch, - std::wstring& appendToStr) -{ - if (L'G' == towupper(ch)) - { - // G (either case) replaces with output redirector > - appendToStr.push_back(L'>'); - return true; - } - return false; -} - -// Routine Description: -// - Checks the given character to see if it is a pipe redirection macro -// and replaces it with the | redirector if there is a match -// Arguments: -// - ch - Character to test as a macro -// - appendToStr - Append the macro result here if it matched -// Return Value: -// - True if we found the macro and appended to the string. -// - False if the given character doesn't match this macro. -bool Alias::s_TryReplacePipeRedirMacro(const wchar_t ch, - std::wstring& appendToStr) -{ - if (L'B' == towupper(ch)) - { - // B (either case) replaces with pipe operator | - appendToStr.push_back(L'|'); - return true; - } - return false; -} - -// Routine Description: -// - Checks the given character to see if it is a next command macro -// and replaces it with CRLF if there is a match -// Arguments: -// - ch - Character to test as a macro -// - appendToStr - Append the macro result here if it matched -// - lineCount - Updates the rolling count of lines if we add a CRLF. -// Return Value: -// - True if we found the macro and appended to the string. -// - False if the given character doesn't match this macro. -bool Alias::s_TryReplaceNextCommandMacro(const wchar_t ch, - std::wstring& appendToStr, - size_t& lineCount) -{ - if (L'T' == towupper(ch)) - { - // T (either case) inserts a CRLF to chain commands - s_AppendCrLf(appendToStr, lineCount); - return true; - } - return false; -} - -// Routine Description: -// - Appends the system line feed (CRLF) to the given string -// Arguments: -// - appendToStr - Append the system line feed here -// - lineCount - Updates the rolling count of lines if we add a CRLF. -void Alias::s_AppendCrLf(std::wstring& appendToStr, - size_t& lineCount) -{ - appendToStr.push_back(L'\r'); - appendToStr.push_back(L'\n'); - lineCount++; -} - -// Routine Description: -// - Searches through the given string for macros and replaces them -// with the matching action -// Arguments: -// - str - On input, the string to search. On output, the string is replaced. -// - tokens - The tokenized command line input. 0 is the alias, 1-N are arguments. -// - fullArgString - Shorthand to 1-N argument string in case of wildcard match. -// Return Value: -// - The number of commands in the final string (line feeds, CRLFs) -size_t Alias::s_ReplaceMacros(std::wstring& str, - const std::deque& tokens, - const std::wstring& fullArgString) -{ - size_t lineCount = 0; - std::wstring finalText; - - // The target text may contain substitution macros indicated by $. - // Walk through and substitute them as appropriate. - for (auto ch = str.cbegin(); ch < str.cend(); ch++) - { - if (L'$' == *ch) - { - // Attempt to read ahead by one character. - const auto chNext = ch + 1; - - if (chNext < str.cend()) - { - auto isProcessed = s_TryReplaceNumberedArgMacro(*chNext, finalText, tokens); - if (!isProcessed) - { - isProcessed = s_TryReplaceWildcardArgMacro(*chNext, finalText, fullArgString); - } - if (!isProcessed) - { - isProcessed = s_TryReplaceInputRedirMacro(*chNext, finalText); - } - if (!isProcessed) - { - isProcessed = s_TryReplaceOutputRedirMacro(*chNext, finalText); - } - if (!isProcessed) - { - isProcessed = s_TryReplacePipeRedirMacro(*chNext, finalText); - } - if (!isProcessed) - { - isProcessed = s_TryReplaceNextCommandMacro(*chNext, finalText, lineCount); - } - if (!isProcessed) - { - // If nothing matches, just push these two characters in. - finalText.push_back(*ch); - finalText.push_back(*chNext); - } - - // Since we read ahead and used that character, - // advance the iterator one extra to compensate. - ch++; - } - else - { - // If no read-ahead, just push this character and be done. - finalText.push_back(*ch); - } - } - else - { - // If it didn't match the macro specifier $, push the character. - finalText.push_back(*ch); - } - } - - // We always terminate with a CRLF to symbolize end of command. - s_AppendCrLf(finalText, lineCount); - - // Give back the final text and count. - str.swap(finalText); - return lineCount; -} - // Routine Description: // - Takes the source text and searches it for an alias belonging to exe name's list. // Arguments: @@ -1113,68 +831,145 @@ size_t Alias::s_ReplaceMacros(std::wstring& str, // - If we found a matching alias, this will be the processed data // and lineCount is updated to the new number of lines. // - If we didn't match and process an alias, return an empty string. -std::wstring Alias::s_MatchAndCopyAlias(std::wstring_view sourceText, const std::wstring& exeName, size_t& lineCount) +std::wstring Alias::s_MatchAndCopyAlias(std::wstring_view sourceText, std::wstring_view exeName, size_t& lineCount) { // Check if we have an EXE in the list that matches the request first. - auto exeIter = g_aliasData.find(exeName); + const auto exeIter = g_aliasData.find(exeName); if (exeIter == g_aliasData.end()) { - // We found no data for this exe. Give back an empty string. - return std::wstring(); + // We found no data for this exe. + return {}; } - auto exeList = exeIter->second; + const auto& exeList = exeIter->second; if (exeList.size() == 0) { - // If there's no match, give back an empty string. - return std::wstring(); + return {}; } - // Tokenize the text by spaces - const auto tokens = s_Tokenize(sourceText); + std::wstring_view args[10]; + size_t argc = 0; - // If there are no tokens, return an empty string - if (tokens.size() == 0) + // Split the source string into whitespace delimited arguments. + for (size_t argBegIdx = 0; argBegIdx < sourceText.size();) { - return std::wstring(); + // Find the end of the current word (= argument). + const auto argEndIdx = sourceText.find_first_of(L' ', argBegIdx); + const auto str = til::safe_slice_abs(sourceText, argBegIdx, argEndIdx); + + // str is empty if the text starting at argBegIdx is whitespace. + // This can only occur if either the source text starts with whitespace or past + // an argument there's only whitespace text left until the end of sourceText. + if (str.empty()) + { + break; + } + + args[argc] = str; + argc++; + + if (argc >= std::size(args)) + { + break; + } + + // Find the start of the next word (= argument). + // If the rest of the text is only whitespace, argBegIdx will be npos + // and the for() loop condition will make us exit. + argBegIdx = sourceText.find_first_not_of(L' ', argEndIdx); } - // Find alias. If there isn't one, return an empty string - const auto alias = tokens.front(); - const auto aliasIter = exeList.find(alias); + // As mentioned above, argc will be 0 if the source text starts with whitespace or only consists of whitespace. + if (argc == 0) + { + return {}; + } + + // The text up to the first space is the alias name. + const auto aliasIter = exeList.find(args[0]); if (aliasIter == exeList.end()) { - // We found no alias pair with this name. Give back an empty string. - return std::wstring(); + return {}; } const auto& target = aliasIter->second; if (target.size() == 0) { - return std::wstring(); + return {}; } - // Get the string of all parameters as a shorthand for $* later. - const auto allParams = s_GetArgString(sourceText); + std::wstring buffer; + size_t lines = 0; - // The final text will be the target but with macros replaced. - auto finalText = target; - lineCount = s_ReplaceMacros(finalText, tokens, allParams); + for (auto it = target.begin(), end = target.end(); it != end;) + { + auto ch = *it++; + if (ch != L'$' || it == end) + { + buffer.push_back(ch); + continue; + } - return finalText; + // $ is our "escape character" and this code handles the escape + // sequence consisting of a single subsequent character. + ch = *it++; + const auto chLower = til::tolower_ascii(ch); + if (chLower >= L'1' && chLower <= L'9') + { + // $1-9 = append the given parameter + const size_t idx = chLower - L'0'; + if (idx < argc) + { + buffer.append(args[idx]); + } + } + else if (chLower == L'*') + { + // $* = append all parameters + if (argc > 1) + { + // args[] is an array of slices into the source text. This appends the text + // starting at first argument up to the end of the source to the buffer. + buffer.append(args[1].data(), sourceText.data() + sourceText.size()); + } + } + else if (chLower == L'l') + { + buffer.push_back(L'<'); + } + else if (chLower == L'g') + { + buffer.push_back(L'>'); + } + else if (chLower == L'b') + { + buffer.push_back(L'|'); + } + else if (chLower == L't') + { + buffer.append(L"\r\n"); + lines++; + } + else + { + buffer.push_back(L'$'); + buffer.push_back(ch); + } + } + + buffer.append(L"\r\n"); + lines++; + + lineCount = lines; + return buffer; } -#ifdef UNIT_TESTING -void Alias::s_TestAddAlias(std::wstring& exe, - std::wstring& alias, - std::wstring& target) +void Alias::s_TestAddAlias(std::wstring exe, std::wstring alias, std::wstring target) { - g_aliasData[exe][alias] = target; + g_aliasData[std::move(exe)][std::move(alias)] = std::move(target); } void Alias::s_TestClearAliases() { g_aliasData.clear(); } - -#endif diff --git a/src/host/alias.h b/src/host/alias.h index 9c8bc38d53..77d6abbd0e 100644 --- a/src/host/alias.h +++ b/src/host/alias.h @@ -16,43 +16,8 @@ class Alias public: static void s_ClearCmdExeAliases(); - static std::wstring s_MatchAndCopyAlias(std::wstring_view sourceText, const std::wstring& exeName, size_t& lineCount); - -private: - static std::deque s_Tokenize(const std::wstring_view str); - static std::wstring s_GetArgString(const std::wstring_view str); - static size_t s_ReplaceMacros(std::wstring& str, - const std::deque& tokens, - const std::wstring& fullArgString); - - static bool s_TryReplaceNumberedArgMacro(const wchar_t ch, - std::wstring& appendToStr, - const std::deque& tokens); - static bool s_TryReplaceWildcardArgMacro(const wchar_t ch, - std::wstring& appendToStr, - const std::wstring fullArgString); - - static bool s_TryReplaceInputRedirMacro(const wchar_t ch, - std::wstring& appendToStr); - static bool s_TryReplaceOutputRedirMacro(const wchar_t ch, - std::wstring& appendToStr); - static bool s_TryReplacePipeRedirMacro(const wchar_t ch, - std::wstring& appendToStr); - - static bool s_TryReplaceNextCommandMacro(const wchar_t ch, - std::wstring& appendToStr, - size_t& lineCount); - - static void s_AppendCrLf(std::wstring& appendToStr, - size_t& lineCount); - -#ifdef UNIT_TESTING - static void s_TestAddAlias(std::wstring& exe, - std::wstring& alias, - std::wstring& target); + static std::wstring s_MatchAndCopyAlias(std::wstring_view sourceText, std::wstring_view exeName, size_t& lineCount); + static void s_TestAddAlias(std::wstring exe, std::wstring alias, std::wstring target); static void s_TestClearAliases(); - - friend class AliasTests; -#endif }; diff --git a/src/host/ut_host/AliasTests.cpp b/src/host/ut_host/AliasTests.cpp index cf499bea89..39c4bf559e 100644 --- a/src/host/ut_host/AliasTests.cpp +++ b/src/host/ut_host/AliasTests.cpp @@ -3,7 +3,6 @@ #include "precomp.h" #include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" #include "alias.h" @@ -63,7 +62,7 @@ class AliasTests BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:exeName", L"{test.exe}") TEST_METHOD_PROPERTY(L"Data:aliasName", L"{foo}") - TEST_METHOD_PROPERTY(L"Data:originalString", L"{ foo one two three four five six seven eight nine ten eleven twelve }") + TEST_METHOD_PROPERTY(L"Data:originalString", L"{ foo one two three four five six seven eight nine ten eleven twelve }") TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" // Each of these is a human-generated test of macro before and after. L"bar=bar%," // The character % will be turned into an \r\n @@ -77,7 +76,7 @@ class AliasTests L"bar $8=bar eight%," L"bar $9=bar nine%," L"bar $3 $1 $4 $1 $5 $9=bar three one four one five nine%," // assorted mixed order parameters with a repeat - L"bar $*=bar one two three four five six seven eight nine ten eleven twelve%," + L"bar $*=bar one two three four five six seven eight nine ten eleven twelve%," L"longer=longer%," // replace with a target longer than the original alias L"redirect $1$goutput $2=redirect one>output two%," // doing these without spaces between some commands L"REDIRECT $1$GOUTPUT $2=REDIRECT one>OUTPUT two%," // also notice we're checking both upper and lowercase @@ -91,7 +90,7 @@ class AliasTests L"MyMoney$$$$$$App=MyMoney$$$$$$App%," // this is a long standing bug, $$ isn't replaced with $. L"Invalid$Apple=Invalid$Apple%," // An invalid macro $A is copied through L"IEndInA$=IEndInA$%," // Ending in a $ is copied through. - L"megamix $7$Gfun $1 $b test $9 $L $2.txt$tall$$the$$things $*$tat$g$gonce.log=megamix seven>fun one | test nine < two.txt%all$$the$$things one two three four five six seven eight nine ten eleven twelve%at>>once.log%" + L"megamix $7$Gfun $1 $b test $9 $L $2.txt$tall$$the$$things $*$tat$g$gonce.log=megamix seven>fun one | test nine < two.txt%all$$the$$things one two three four five six seven eight nine ten eleven twelve%at>>once.log%" L"}") END_TEST_METHOD_PROPERTIES() @@ -181,258 +180,4 @@ class AliasTests VERIFY_IS_TRUE(buffer.empty()); VERIFY_ARE_EQUAL(1u, dwLines); } - - TEST_METHOD(Tokenize) - { - std::wstring tokenStr(L"one two three"); - std::deque tokensExpected; - tokensExpected.emplace_back(L"one"); - tokensExpected.emplace_back(L"two"); - tokensExpected.emplace_back(L"three"); - - auto tokensActual = Alias::s_Tokenize(tokenStr); - - VERIFY_ARE_EQUAL(tokensExpected.size(), tokensActual.size()); - - for (size_t i = 0; i < tokensExpected.size(); i++) - { - VERIFY_ARE_EQUAL(String(tokensExpected[i].data()), String(tokensActual[i].data())); - } - } - - TEST_METHOD(TokenizeNothing) - { - std::wstring tokenStr(L"alias"); - std::deque tokensExpected; - tokensExpected.emplace_back(tokenStr); - - auto tokensActual = Alias::s_Tokenize(tokenStr); - - VERIFY_ARE_EQUAL(tokensExpected.size(), tokensActual.size()); - - for (size_t i = 0; i < tokensExpected.size(); i++) - { - VERIFY_ARE_EQUAL(String(tokensExpected[i].data()), String(tokensActual[i].data())); - } - } - - TEST_METHOD(GetArgString) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"alias arg1 arg2 arg3=arg1 arg2 arg3," - L"aliasOnly=" - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - auto actual = Alias::s_GetArgString(target); - - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - } - - TEST_METHOD(NumberedArgMacro) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"1=one," - L"2=two," - L"3=three," - L"4=four," - L"5=five," - L"6=six," - L"7=seven," - L"8=eight," - L"9=nine," - L"A=," - L"0=," - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - std::deque tokens; - tokens.emplace_back(L"alias"); - tokens.emplace_back(L"one"); - tokens.emplace_back(L"two"); - tokens.emplace_back(L"three"); - tokens.emplace_back(L"four"); - tokens.emplace_back(L"five"); - tokens.emplace_back(L"six"); - tokens.emplace_back(L"seven"); - tokens.emplace_back(L"eight"); - tokens.emplace_back(L"nine"); - tokens.emplace_back(L"ten"); - - // if we expect non-empty results, then we should get a bool back saying it was processed - const auto returnExpected = !expected.empty(); - - std::wstring actual; - const auto returnActual = Alias::s_TryReplaceNumberedArgMacro(target[0], actual, tokens); - - VERIFY_ARE_EQUAL(returnExpected, returnActual); - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - } - - TEST_METHOD(WildcardArgMacro) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"*=one two three," - L"A=," - L"0=," - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - std::wstring fullArgString(L"one two three"); - - // if we expect non-empty results, then we should get a bool back saying it was processed - const auto returnExpected = !expected.empty(); - - std::wstring actual; - const auto returnActual = Alias::s_TryReplaceWildcardArgMacro(target[0], actual, fullArgString); - - VERIFY_ARE_EQUAL(returnExpected, returnActual); - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - } - - TEST_METHOD(InputRedirMacro) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"L=<," - L"l=<," - L"A=," - L"a=," - L"0=," - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - // if we expect non-empty results, then we should get a bool back saying it was processed - const auto returnExpected = !expected.empty(); - - std::wstring actual; - const auto returnActual = Alias::s_TryReplaceInputRedirMacro(target[0], actual); - - VERIFY_ARE_EQUAL(returnExpected, returnActual); - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - } - - TEST_METHOD(OutputRedirMacro) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"G=>," - L"g=>," - L"A=," - L"a=," - L"0=," - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - // if we expect non-empty results, then we should get a bool back saying it was processed - const auto returnExpected = !expected.empty(); - - std::wstring actual; - const auto returnActual = Alias::s_TryReplaceOutputRedirMacro(target[0], actual); - - VERIFY_ARE_EQUAL(returnExpected, returnActual); - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - } - - TEST_METHOD(PipeRedirMacro) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"B=|," - L"b=|," - L"A=," - L"a=," - L"0=," - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - // if we expect non-empty results, then we should get a bool back saying it was processed - const auto returnExpected = !expected.empty(); - - std::wstring actual; - const auto returnActual = Alias::s_TryReplacePipeRedirMacro(target[0], actual); - - VERIFY_ARE_EQUAL(returnExpected, returnActual); - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - } - - TEST_METHOD(NextCommandMacro) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"T=%," - L"t=%," - L"A=," - L"a=," - L"0=," - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - _ReplacePercentWithCRLF(expected); - - // if we expect non-empty results, then we should get a bool back saying it was processed - const auto returnExpected = !expected.empty(); - - std::wstring actual; - size_t lineCountActual = 0; - - const auto lineCountExpected = lineCountActual + (returnExpected ? 1 : 0); - - const auto returnActual = Alias::s_TryReplaceNextCommandMacro(target[0], actual, lineCountActual); - - VERIFY_ARE_EQUAL(returnExpected, returnActual); - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - VERIFY_ARE_EQUAL(lineCountExpected, lineCountActual); - } - - TEST_METHOD(AppendCrLf) - { - std::wstring actual; - size_t lineCountActual = 0; - - const std::wstring expected(L"\r\n"); - const auto lineCountExpected = lineCountActual + 1; - - Alias::s_AppendCrLf(actual, lineCountActual); - VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); - VERIFY_ARE_EQUAL(lineCountExpected, lineCountActual); - } }; diff --git a/src/inc/til.h b/src/inc/til.h index c1d799fe07..eb781634fe 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -55,7 +55,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { template - as_view_t clamp_slice_abs(const T& view, size_t beg, size_t end) + as_view_t safe_slice_abs(const T& view, size_t beg, size_t end) { const auto len = view.size(); end = std::min(end, len); @@ -64,7 +64,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } template - as_view_t clamp_slice_len(const T& view, size_t start, size_t count) + as_view_t safe_slice_len(const T& view, size_t start, size_t count) { const auto len = view.size(); start = std::min(start, len); diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 43b636336a..e5b3d061f5 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -1524,7 +1524,7 @@ BackendD3D::ShadingType BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p const auto width = static_cast(p.s->font->softFontCellSize.width); const auto height = static_cast(p.s->font->softFontCellSize.height); const auto softFontIndex = glyphIndex - 0xEF20u; - const auto data = til::clamp_slice_len(p.s->font->softFontPattern, height * softFontIndex, height); + const auto data = til::safe_slice_len(p.s->font->softFontPattern, height * softFontIndex, height); // This happens if someone wrote a U+EF2x character (by accident), but we don't even have soft fonts enabled yet. if (data.empty() || data.size() != height) From d4faf98455feb57dec8f573edef4b760ebe77495 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 3 May 2024 01:26:03 +0200 Subject: [PATCH 43/53] Fix remaining buffer serialization bugs (#17182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It may be more accurate to say: "Fix _known_ remaining buffer serialization bugs", but I'll try to be positive about my code. Initially, the buffer is initialized with the default attributes, but once it begins to scroll, newly scrolled in rows are initialized with the current attributes. This means we need to set the current attributes to those of the upcoming row before the row comes up. This is related to #17074. ## Validation Steps Performed * Persist and restore a buffer 10 times * All previous "Restore" status messages look correct ✅ * The escape sequences in the buffer file look correct ✅ --- src/buffer/out/textBuffer.cpp | 52 +++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 2aa9176a8c..f0c893bc60 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -2559,6 +2559,7 @@ void TextBuffer::Serialize(const wchar_t* destination) const TextColor previousBg; TextColor previousUl; uint16_t previousHyperlinkId = 0; + bool delayedLineBreak = false; // This iterates through each row. The exit condition is at the end // of the for() loop so that we can properly handle file flushing. @@ -2578,9 +2579,11 @@ void TextBuffer::Serialize(const wchar_t* destination) const } const auto& runs = row.Attributes().runs(); - auto it = runs.begin(); + const auto beg = runs.begin(); const auto end = runs.end(); + auto it = beg; const auto last = end - 1; + const auto lastCharX = row.MeasureRight(); til::CoordType oldX = 0; for (; it != end; ++it) @@ -2760,24 +2763,55 @@ void TextBuffer::Serialize(const wchar_t* destination) const } } - auto newX = oldX + it->length; - // Trim whitespace with default attributes from the end of each line. - if (it == last && it->value == TextAttribute{}) + // Initially, the buffer is initialized with the default attributes, but once it begins to scroll, + // newly scrolled in rows are initialized with the current attributes. This means we need to set + // the current attributes to those of the upcoming row before the row comes up. Or inversely: + // We let the row come up, let it set its attributes and only then print the newline. + if (delayedLineBreak) { - // This can result in oldX > newX, but that's okay because GetText() - // is robust against that and returns an empty string. - newX = row.MeasureRight(); + buffer.append(L"\r\n"); + delayedLineBreak = false; + } + + auto newX = oldX + it->length; + + // Since our text buffer doesn't store the original input text, the information over the amount of trailing + // whitespaces was lost. If we don't do anything here then a row that just says "Hello" would be serialized + // to "Hello ...". If the user restores the buffer dump with a different window size, + // this would result in some fairly ugly reflow. This code attempts to at least trim trailing whitespaces. + // + // As mentioned above for `delayedLineBreak`, rows are initialized with their first attribute, BUT + // only if the viewport has begun to scroll. Otherwise, they're initialized with the default attributes. + // In other words, we can only skip \x1b[K = Erase in Line, if both the first/last attribute are the default attribute. + static constexpr TextAttribute defaultAttr; + const auto trimTrailingWhitespaces = it == last && lastCharX < newX; + const auto clearToEndOfLine = trimTrailingWhitespaces && beg->value != defaultAttr || beg->value != defaultAttr; + + if (trimTrailingWhitespaces) + { + newX = lastCharX; } buffer.append(row.GetText(oldX, newX)); + + if (clearToEndOfLine) + { + buffer.append(L"\x1b[K"); + } + oldX = newX; } const auto moreRowsRemaining = currentRow < lastRowWithText; + delayedLineBreak = !row.WasWrapForced(); - if (!row.WasWrapForced() || !moreRowsRemaining) + if (!moreRowsRemaining) { - buffer.append(L"\r\n"); + if (previousHyperlinkId) + { + buffer.append(L"\x1b]8;;\x1b\\"); + } + buffer.append(L"\x1b[m\r\n"); } if (buffer.size() >= writeThreshold || !moreRowsRemaining) From 92e05f246a7cc59262c42687bbcab15ecf0f109e Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 2 May 2024 18:27:12 -0500 Subject: [PATCH 44/53] Fix clearing marks (#17144) Tests are good, I should write more of them. Closes #17130 --- src/buffer/out/textBuffer.cpp | 24 +-- .../ConptyRoundtripTests.cpp | 139 +++++++++++++++--- src/terminal/adapter/adaptDispatch.cpp | 4 - 3 files changed, 136 insertions(+), 31 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index f0c893bc60..7ef4ffc552 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1176,36 +1176,40 @@ void TextBuffer::Reset() noexcept _initialAttributes = _currentAttributes; } -void TextBuffer::ClearScrollback(const til::CoordType start, const til::CoordType height) +// Arguments: +// - newFirstRow: The current y-position of the viewport. We'll clear up until here. +// - rowsToKeep: the number of rows to keep in the buffer. +void TextBuffer::ClearScrollback(const til::CoordType newFirstRow, const til::CoordType rowsToKeep) { - if (start <= 0) + // We're already at the top? don't clear anything. There's no scrollback. + if (newFirstRow <= 0) { return; } - - if (height <= 0) + // The new viewport should keep 0 rows? Then just reset everything. + if (rowsToKeep <= 0) { _decommit(); return; } + ClearMarksInRange(til::point{ 0, 0 }, til::point{ _width, std::max(0, newFirstRow - 1) }); + // Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can // MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer. - // The start parameter is relative to the _firstRow. The trick to get the content to the absolute start + // The newFirstRow parameter is relative to the _firstRow. The trick to get the content to the absolute start // is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into // the absolute start while reading from relative coordinates. This works because GetRowByOffset() // operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue. - const auto startAbsolute = _firstRow + start; + const auto startAbsolute = _firstRow + newFirstRow; _firstRow = 0; - ScrollRows(startAbsolute, height, -startAbsolute); + ScrollRows(startAbsolute, rowsToKeep, -startAbsolute); const auto end = _estimateOffsetOfLastCommittedRow(); - for (auto y = height; y <= end; ++y) + for (auto y = rowsToKeep; y <= end; ++y) { GetMutableRowByOffset(y).Reset(_initialAttributes); } - - ClearMarksInRange(til::point{ 0, height }, til::point{ _width, _height }); } // Routine Description: diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 4c582d0f44..2dc64562e8 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -239,12 +239,14 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final TEST_METHOD(MultilinePromptRegions); TEST_METHOD(ManyMultilinePromptsWithTrailingSpaces); TEST_METHOD(ReflowPromptRegions); + TEST_METHOD(ClearMarksTest); private: bool _writeCallback(const char* const pch, const size_t cch); void _flushFirstFrame(); void _resizeConpty(const til::CoordType sx, const til::CoordType sy); void _clearConpty(); + void _clear(int clearBufferMethod, SCREEN_INFORMATION& si); [[nodiscard]] std::tuple _performResize(const til::size newSize); @@ -2720,9 +2722,6 @@ void ConptyRoundtripTests::ClsAndClearHostClearsScrollbackTest() BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:clearBufferMethod", L"{0, 1, 2}") END_TEST_METHOD_PROPERTIES(); - constexpr auto ClearLikeCls = 0; - constexpr auto ClearLikeClearHost = 1; - constexpr auto ClearWithVT = 2; INIT_TEST_PROPERTY(int, clearBufferMethod, L"Controls whether we clear the buffer like cmd or like powershell"); Log::Comment(L"This test checks the shims for cmd.exe and powershell.exe. " @@ -2789,6 +2788,31 @@ void ConptyRoundtripTests::ClsAndClearHostClearsScrollbackTest() VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions()); VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize()); + _clear(clearBufferMethod, si); + + VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions()); + VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize()); + + Log::Comment(L"Painting the frame"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); + + Log::Comment(L"========== Checking the host buffer state (after) =========="); + verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), true); + + Log::Comment(L"Painting the frame"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); + Log::Comment(L"========== Checking the terminal buffer state (after) =========="); + verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true); +} + +void ConptyRoundtripTests::_clear(int clearBufferMethod, SCREEN_INFORMATION& si) +{ + constexpr auto ClearLikeCls = 0; + constexpr auto ClearLikeClearHost = 1; + constexpr auto ClearWithVT = 2; + + auto& sm = si.GetStateMachine(); + if (clearBufferMethod == ClearLikeCls) { // Execute the cls, EXACTLY LIKE CMD. @@ -2840,20 +2864,6 @@ void ConptyRoundtripTests::ClsAndClearHostClearsScrollbackTest() sm.ProcessString(L"\x1b[3J"); } - - VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions()); - VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize()); - - Log::Comment(L"Painting the frame"); - VERIFY_SUCCEEDED(renderer.PaintFrame()); - - Log::Comment(L"========== Checking the host buffer state (after) =========="); - verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), true); - - Log::Comment(L"Painting the frame"); - VERIFY_SUCCEEDED(renderer.PaintFrame()); - Log::Comment(L"========== Checking the terminal buffer state (after) =========="); - verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true); } void ConptyRoundtripTests::TestResizeWithCookedRead() @@ -4945,3 +4955,98 @@ void ConptyRoundtripTests::ReflowPromptRegions() Log::Comment(L"========== Checking the terminal buffer state (after) =========="); verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true, true); } + +void ConptyRoundtripTests::ClearMarksTest() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:clearBufferMethod", L"{0, 1, 2}") + END_TEST_METHOD_PROPERTIES(); + + INIT_TEST_PROPERTY(int, clearBufferMethod, L"Controls whether we clear the buffer like cmd or like powershell"); + + Log::Comment(L"This test checks the shims for cmd.exe and powershell.exe. " + L"Their build in commands for clearing the console buffer " + L"should work to clear the terminal buffer, not just the " + L"terminal viewport."); + + auto& g = ServiceLocator::LocateGlobals(); + auto& renderer = *g.pRender; + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto* hostTb = &si.GetTextBuffer(); + auto* termTb = term->_mainBuffer.get(); + + auto& sm = si.GetStateMachine(); + + _flushFirstFrame(); + + _checkConptyOutput = false; + _logConpty = false; + + const auto hostView = si.GetViewport(); + const auto end = 2 * hostView.Height(); + + auto writePrompt = [](StateMachine& stateMachine, const auto& path) { + // A prompt looks like: + // `PWSH C:\> ` + // + // which is 10 characters for "C:\" + stateMachine.ProcessString(FTCS_D); + stateMachine.ProcessString(FTCS_A); + stateMachine.ProcessString(L"\x1b]9;9;"); + stateMachine.ProcessString(path); + stateMachine.ProcessString(L"\x7"); + stateMachine.ProcessString(L"PWSH "); + stateMachine.ProcessString(path); + stateMachine.ProcessString(L"> "); + stateMachine.ProcessString(FTCS_B); + }; + auto writeCommand = [](StateMachine& stateMachine, const auto& cmd) { + stateMachine.ProcessString(cmd); + stateMachine.ProcessString(FTCS_C); + stateMachine.ProcessString(L"\r\n"); + }; + + for (auto i = 0; i < end; i++) + { + writePrompt(sm, L"C:\\"); + writeCommand(sm, L"Foo-bar"); + sm.ProcessString(L"This is some text \r\n"); + } + writePrompt(sm, L"C:\\"); + + auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& /*viewport*/, const bool afterClear = false) { + const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope; + const auto marks = tb.GetMarkExtents(); + if (afterClear) + { + VERIFY_ARE_EQUAL(0u, marks.size()); + } + else + { + VERIFY_IS_GREATER_THAN(marks.size(), 1u, L"There should be at least one mark"); + } + }; + + Log::Comment(L"========== Checking the host buffer state (before) =========="); + verifyBuffer(*hostTb, si.GetViewport().ToExclusive()); + + Log::Comment(L"Painting the frame"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); + + Log::Comment(L"========== Checking the terminal buffer state (before) =========="); + verifyBuffer(*termTb, term->_mutableViewport.ToExclusive()); + + VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions()); + VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize()); + + _clear(clearBufferMethod, si); + + VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions()); + VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize()); + + Log::Comment(L"Painting the frame"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); + Log::Comment(L"========== Checking the terminal buffer state (after) =========="); + verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true); +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 34bc5cd80e..bf038dd75e 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -3294,10 +3294,6 @@ bool AdaptDispatch::_EraseAll() // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom); - // Clear any marks that remain below the start of the - textBuffer.ClearMarksInRange(til::point{ 0, newViewportTop }, - til::point{ bufferSize.Width(), bufferSize.Height() }); - // GH#5683 - If this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this ED sequence to the connected // terminal application. While we're in conpty mode, when the client From 4fbcd65e1abc073e510b3a532c2a40a34d9af734 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 3 May 2024 01:33:25 +0200 Subject: [PATCH 45/53] Fix multiple cursor invalidation issues (#17181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There were multiple bugs: * GDI engine only paints whatever has been invalidated. This means we need to not just invalidate the old cursor rect but also the new one, or else movements may not be visible. * The composition should be drawn at the cursor position even if the cursor is invisible, but inside the renderer viewport. * Conceptually, scrolling the viewport moves the relative cursor position even if the cursor is invisible. * An invisible cursor is not the same as one that's outside the viewport. It's more like a cursor that's not turned on. To resolve the first issue we simply need to call `InvalidateCursor` again. To do so, it was abstracted into `_invalidateCurrentCursor()`. The next 2 issues are resolved by un-`optional`-izing `CursorOptions`. After all, even an invisible or an out-of-bounds cursor still has a coordinate and it may still be scrolled into view. Instead, it has the new `inViewport` property as a replacement. This allows for instance the IME composition code in the renderer to use the cursor coordinate while the cursor is invisible. The last issue is fixed by simply changing the `.isOn` logic. Closes #17150 ## Validation Steps Performed * In conhost with the GDI renderer: `printf "\e[2 q"; sleep 2; printf "\e[A"; sleep 2; printf "\e[B"` Cursor moves up after 2s and then down again after 2s. ✅ * Hide the cursor (`"\e[?25l"`) and use a CJK IME. Words can still be written and deleted correctly. ✅ * Turning the cursor back on (`"\e[?25h"`) works ✅ * Scrolling shows/hides the cursor ✅ --- src/renderer/base/renderer.cpp | 296 +++++++++++++++++-------------- src/renderer/base/renderer.hpp | 7 +- src/renderer/inc/CursorOptions.h | 5 + 3 files changed, 170 insertions(+), 138 deletions(-) diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 82b1eaea1b..91e5772006 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -121,96 +121,14 @@ IRenderData* Renderer::GetRenderData() const noexcept // Last chance check if anything scrolled without an explicit invalidate notification since the last frame. _CheckViewportAndScroll(); - if (_currentCursorOptions) - { - const auto& buffer = _pData->GetTextBuffer(); - const auto view = buffer.GetSize(); - const auto coord = _currentCursorOptions->coordCursor; + _invalidateCurrentCursor(); // Invalidate the previous cursor position. + _invalidateOldComposition(); - // If we had previously drawn a composition at the previous cursor position - // we need to invalidate the entire line because who knows what changed. - // (It's possible to figure that out, but not worth the effort right now.) - if (_compositionCache) - { - til::rect rect{ 0, coord.y, til::CoordTypeMax, coord.y + 1 }; - if (view.TrimToViewport(&rect)) - { - FOREACH_ENGINE(pEngine) - { - LOG_IF_FAILED(pEngine->Invalidate(&rect)); - } - } - } - else - { - const auto lineRendition = buffer.GetLineRendition(coord.y); - const auto cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1; - - til::rect rect{ coord.x, coord.y, coord.x + cursorWidth, coord.y + 1 }; - rect = BufferToScreenLine(rect, lineRendition); - - if (view.TrimToViewport(&rect)) - { - FOREACH_ENGINE(pEngine) - { - LOG_IF_FAILED(pEngine->InvalidateCursor(&rect)); - } - } - } - } - - _currentCursorOptions = _GetCursorInfo(); + _updateCursorInfo(); _compositionCache.reset(); - // Invalidate the line that the active TSF composition is on, - // so that _PaintBufferOutput() actually gets a chance to draw it. - if (!_pData->activeComposition.text.empty()) - { - const auto viewport = _pData->GetViewport(); - const auto coordCursor = _pData->GetCursorPosition(); - - til::rect line{ 0, coordCursor.y, til::CoordTypeMax, coordCursor.y + 1 }; - if (viewport.TrimToViewport(&line)) - { - viewport.ConvertToOrigin(&line); - - FOREACH_ENGINE(pEngine) - { - LOG_IF_FAILED(pEngine->Invalidate(&line)); - } - - auto& buffer = _pData->GetTextBuffer(); - auto& scratch = buffer.GetScratchpadRow(); - - std::wstring_view text{ _pData->activeComposition.text }; - RowWriteState state{ - .columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(), - }; - - state.text = text.substr(0, _pData->activeComposition.cursorPos); - scratch.ReplaceText(state); - const auto cursorOffset = state.columnEnd; - - state.text = text.substr(_pData->activeComposition.cursorPos); - state.columnBegin = state.columnEnd; - scratch.ReplaceText(state); - - // Ideally the text is inserted at the position of the cursor (`coordCursor`), - // but if we got more text than fits into the remaining space until the end of the line, - // then we'll insert the text aligned to the end of the line. - const auto remaining = state.columnLimit - state.columnEnd; - const auto beg = std::clamp(coordCursor.x, 0, remaining); - - const auto baseAttribute = buffer.GetRowByOffset(coordCursor.y).GetAttrByColumn(coordCursor.x); - _compositionCache.emplace(til::point{ beg, coordCursor.y }, baseAttribute); - - // Fake-move the cursor to where it needs to be in the active composition. - if (_currentCursorOptions) - { - _currentCursorOptions->coordCursor.x = std::min(beg + cursorOffset, line.right - 1); - } - } - } + _invalidateCurrentCursor(); // Invalidate the new cursor position. + _prepareNewComposition(); FOREACH_ENGINE(pEngine) { @@ -510,6 +428,20 @@ bool Renderer::_CheckViewportAndScroll() } _ScrollPreviousSelection(coordDelta); + + // The cursor may have moved out of or into the viewport. Update the .inViewport property. + { + const auto view = ScreenToBufferLine(srNewViewport, _currentCursorOptions.lineRendition); + const auto coordCursor = _currentCursorOptions.coordCursor; + + // Note that we allow the X coordinate to be outside the left border by 1 position, + // because the cursor could still be visible if the focused character is double width. + const auto xInRange = coordCursor.x >= view.left - 1 && coordCursor.x <= view.right; + const auto yInRange = coordCursor.y >= view.top && coordCursor.y <= view.bottom; + + _currentCursorOptions.inViewport = xInRange && yInRange; + } + return true; } @@ -1188,58 +1120,153 @@ bool Renderer::_isInHoveredInterval(const til::point coordTarget) const noexcept // - // Return Value: // - nullopt if the cursor is off or out-of-frame, otherwise a CursorOptions -[[nodiscard]] std::optional Renderer::_GetCursorInfo() +void Renderer::_updateCursorInfo() { - if (_pData->IsCursorVisible()) + // Get cursor position in buffer + auto coordCursor = _pData->GetCursorPosition(); + + // GH#3166: Only draw the cursor if it's actually in the viewport. It + // might be on the line that's in that partially visible row at the + // bottom of the viewport, the space that's not quite a full line in + // height. Since we don't draw that text, we shouldn't draw the cursor + // there either. + + // The cursor is never rendered as double height, so we don't care about + // the exact line rendition - only whether it's double width or not. + const auto doubleWidth = _pData->GetTextBuffer().IsDoubleWidthLine(coordCursor.y); + const auto lineRendition = doubleWidth ? LineRendition::DoubleWidth : LineRendition::SingleWidth; + + // We need to convert the screen coordinates of the viewport to an + // equivalent range of buffer cells, taking line rendition into account. + const auto viewport = _pData->GetViewport().ToInclusive(); + const auto view = ScreenToBufferLine(viewport, lineRendition); + + // Note that we allow the X coordinate to be outside the left border by 1 position, + // because the cursor could still be visible if the focused character is double width. + const auto xInRange = coordCursor.x >= view.left - 1 && coordCursor.x <= view.right; + const auto yInRange = coordCursor.y >= view.top && coordCursor.y <= view.bottom; + + // Adjust cursor Y offset to viewport. + // The viewport X offset is saved in the options and handled with a transform. + coordCursor.y -= view.top; + + const auto cursorColor = _renderSettings.GetColorTableEntry(TextColor::CURSOR_COLOR); + const auto useColor = cursorColor != INVALID_COLOR; + + _currentCursorOptions.coordCursor = coordCursor; + _currentCursorOptions.viewportLeft = viewport.left; + _currentCursorOptions.lineRendition = lineRendition; + _currentCursorOptions.ulCursorHeightPercent = _pData->GetCursorHeight(); + _currentCursorOptions.cursorPixelWidth = _pData->GetCursorPixelWidth(); + _currentCursorOptions.fIsDoubleWidth = _pData->IsCursorDoubleWidth(); + _currentCursorOptions.cursorType = _pData->GetCursorStyle(); + _currentCursorOptions.fUseColor = useColor; + _currentCursorOptions.cursorColor = cursorColor; + _currentCursorOptions.isVisible = _pData->IsCursorVisible(); + _currentCursorOptions.isOn = _currentCursorOptions.isVisible && _pData->IsCursorOn(); + _currentCursorOptions.inViewport = xInRange && yInRange; +} + +void Renderer::_invalidateCurrentCursor() const +{ + if (!_currentCursorOptions.inViewport || !_currentCursorOptions.isOn) { - // Get cursor position in buffer - auto coordCursor = _pData->GetCursorPosition(); + return; + } - // GH#3166: Only draw the cursor if it's actually in the viewport. It - // might be on the line that's in that partially visible row at the - // bottom of the viewport, the space that's not quite a full line in - // height. Since we don't draw that text, we shouldn't draw the cursor - // there either. + const auto& buffer = _pData->GetTextBuffer(); + const auto view = buffer.GetSize(); + const auto coord = _currentCursorOptions.coordCursor; - // The cursor is never rendered as double height, so we don't care about - // the exact line rendition - only whether it's double width or not. - const auto doubleWidth = _pData->GetTextBuffer().IsDoubleWidthLine(coordCursor.y); - const auto lineRendition = doubleWidth ? LineRendition::DoubleWidth : LineRendition::SingleWidth; + const auto lineRendition = buffer.GetLineRendition(coord.y); + const auto cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1; - // We need to convert the screen coordinates of the viewport to an - // equivalent range of buffer cells, taking line rendition into account. - const auto view = ScreenToBufferLine(_pData->GetViewport().ToInclusive(), lineRendition); + til::rect rect{ coord.x, coord.y, coord.x + cursorWidth, coord.y + 1 }; + rect = BufferToScreenLine(rect, lineRendition); - // Note that we allow the X coordinate to be outside the left border by 1 position, - // because the cursor could still be visible if the focused character is double width. - const auto xInRange = coordCursor.x >= view.left - 1 && coordCursor.x <= view.right; - const auto yInRange = coordCursor.y >= view.top && coordCursor.y <= view.bottom; - if (xInRange && yInRange) + if (view.TrimToViewport(&rect)) + { + FOREACH_ENGINE(pEngine) { - // Adjust cursor Y offset to viewport. - // The viewport X offset is saved in the options and handled with a transform. - coordCursor.y -= view.top; - - auto cursorColor = _renderSettings.GetColorTableEntry(TextColor::CURSOR_COLOR); - auto useColor = cursorColor != INVALID_COLOR; - - // Build up the cursor parameters including position, color, and drawing options - CursorOptions options; - options.coordCursor = coordCursor; - options.viewportLeft = _pData->GetViewport().Left(); - options.lineRendition = lineRendition; - options.ulCursorHeightPercent = _pData->GetCursorHeight(); - options.cursorPixelWidth = _pData->GetCursorPixelWidth(); - options.fIsDoubleWidth = _pData->IsCursorDoubleWidth(); - options.cursorType = _pData->GetCursorStyle(); - options.fUseColor = useColor; - options.cursorColor = cursorColor; - options.isOn = _pData->IsCursorOn(); - - return { options }; + LOG_IF_FAILED(pEngine->InvalidateCursor(&rect)); } } - return std::nullopt; +} + +// If we had previously drawn a composition at the previous cursor position +// we need to invalidate the entire line because who knows what changed. +// (It's possible to figure that out, but not worth the effort right now.) +void Renderer::_invalidateOldComposition() const +{ + if (!_compositionCache || !_currentCursorOptions.inViewport) + { + return; + } + + const auto& buffer = _pData->GetTextBuffer(); + const auto view = buffer.GetSize(); + const auto coord = _currentCursorOptions.coordCursor; + + til::rect rect{ 0, coord.y, til::CoordTypeMax, coord.y + 1 }; + if (view.TrimToViewport(&rect)) + { + FOREACH_ENGINE(pEngine) + { + LOG_IF_FAILED(pEngine->Invalidate(&rect)); + } + } +} + +// Invalidate the line that the active TSF composition is on, +// so that _PaintBufferOutput() actually gets a chance to draw it. +void Renderer::_prepareNewComposition() +{ + if (_pData->activeComposition.text.empty()) + { + return; + } + + const auto viewport = _pData->GetViewport(); + const auto coordCursor = _pData->GetCursorPosition(); + + til::rect line{ 0, coordCursor.y, til::CoordTypeMax, coordCursor.y + 1 }; + if (viewport.TrimToViewport(&line)) + { + viewport.ConvertToOrigin(&line); + + FOREACH_ENGINE(pEngine) + { + LOG_IF_FAILED(pEngine->Invalidate(&line)); + } + + auto& buffer = _pData->GetTextBuffer(); + auto& scratch = buffer.GetScratchpadRow(); + + std::wstring_view text{ _pData->activeComposition.text }; + RowWriteState state{ + .columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(), + }; + + state.text = text.substr(0, _pData->activeComposition.cursorPos); + scratch.ReplaceText(state); + const auto cursorOffset = state.columnEnd; + + state.text = text.substr(_pData->activeComposition.cursorPos); + state.columnBegin = state.columnEnd; + scratch.ReplaceText(state); + + // Ideally the text is inserted at the position of the cursor (`coordCursor`), + // but if we got more text than fits into the remaining space until the end of the line, + // then we'll insert the text aligned to the end of the line. + const auto remaining = state.columnLimit - state.columnEnd; + const auto beg = std::clamp(coordCursor.x, 0, remaining); + + const auto baseAttribute = buffer.GetRowByOffset(coordCursor.y).GetAttrByColumn(coordCursor.x); + _compositionCache.emplace(til::point{ beg, coordCursor.y }, baseAttribute); + + // Fake-move the cursor to where it needs to be in the active composition. + _currentCursorOptions.coordCursor.x = std::min(beg + cursorOffset, line.right - 1); + } } // Routine Description: @@ -1250,9 +1277,9 @@ bool Renderer::_isInHoveredInterval(const til::point coordTarget) const noexcept // - void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine) { - if (_currentCursorOptions) + if (_currentCursorOptions.inViewport && _currentCursorOptions.isVisible) { - LOG_IF_FAILED(pEngine->PaintCursor(*_currentCursorOptions)); + LOG_IF_FAILED(pEngine->PaintCursor(_currentCursorOptions)); } } @@ -1385,10 +1412,7 @@ void Renderer::_ScrollPreviousSelection(const til::point delta) rc += delta; } - if (_currentCursorOptions) - { - _currentCursorOptions->coordCursor += delta; - } + _currentCursorOptions.coordCursor += delta; } } diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 1944beb4b9..8c4308c5a8 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -122,7 +122,10 @@ namespace Microsoft::Console::Render void _ScrollPreviousSelection(const til::point delta); [[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine); bool _isInHoveredInterval(til::point coordTarget) const noexcept; - [[nodiscard]] std::optional _GetCursorInfo(); + void _updateCursorInfo(); + void _invalidateCurrentCursor() const; + void _invalidateOldComposition() const; + void _prepareNewComposition(); [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); const RenderSettings& _renderSettings; @@ -134,7 +137,7 @@ namespace Microsoft::Console::Render uint16_t _hyperlinkHoveredId = 0; std::optional::interval> _hoveredInterval; Microsoft::Console::Types::Viewport _viewport; - std::optional _currentCursorOptions; + CursorOptions _currentCursorOptions; std::optional _compositionCache; std::vector _clusterBuffer; std::vector _previousSelection; diff --git a/src/renderer/inc/CursorOptions.h b/src/renderer/inc/CursorOptions.h index d3c766c7b5..a030426114 100644 --- a/src/renderer/inc/CursorOptions.h +++ b/src/renderer/inc/CursorOptions.h @@ -49,9 +49,14 @@ namespace Microsoft::Console::Render // Color to use for drawing instead of the default COLORREF cursorColor; + // The other kind of on/off state for the cursor, because VtEngine needs it to handle \x1b[?25l/h. + bool isVisible; // Is the cursor currently visually visible? // If the cursor has blinked off, this is false. // if the cursor has blinked on, this is true. bool isOn; + + // Is the cursor within the viewport of the renderer? + bool inViewport; }; } From 5c758974e5b303dab0307d2183957fd4d9418338 Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Thu, 2 May 2024 18:39:10 -0500 Subject: [PATCH 46/53] Localization Updates - main - 05/02/2024 23:37:23 (#17185) --- .../TerminalSettingsEditor/Resources/de-DE/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/fr-FR/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/ru-RU/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/zh-CN/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/zh-TW/Resources.resw | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw index eb3aee0ac6..27e9ace6da 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw @@ -733,10 +733,16 @@ Durchsuchen… Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Neue Schriftachse hinzufügen + Neu hinzufügen Button label that adds a new font axis for the current font. + + Neues Schriftmerkmal hinzufügen + Neu hinzufügen Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw index 0d2433ba1d..7f2a42f5f3 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw @@ -733,10 +733,16 @@ Parcourir... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Ajouter un nouvel axe de police + Ajouter nouveau Button label that adds a new font axis for the current font. + + Ajouter une nouvelle fonctionnalité de police + Ajouter nouveau Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw index 4c18e988ea..592a519ebd 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw @@ -733,10 +733,16 @@ Открыть... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Добавить новую ось шрифта + Добавить Button label that adds a new font axis for the current font. + + Добавить новую функцию шрифта + Добавить Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw index 298c9279e9..6461b72290 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw @@ -733,10 +733,16 @@ 浏览…… Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + 添加新字体轴 + 新增 Button label that adds a new font axis for the current font. + + 添加新字体功能 + 新增 Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw index ccc0bb4720..f951dbc2d9 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw @@ -733,10 +733,16 @@ 瀏覽... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + 新增字型座標軸 + 新增 Button label that adds a new font axis for the current font. + + 新增字型功能 + 新增 Button label that adds a new font feature for the current font. From a0d1329d7aabf72301a0cdaec420bd165a6545d3 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 2 May 2024 18:45:19 -0500 Subject: [PATCH 47/53] version: bump to 1.22 on main --- custom.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom.props b/custom.props index 6a0586fd8b..3227f85d7e 100644 --- a/custom.props +++ b/custom.props @@ -5,7 +5,7 @@ true 2024 1 - 21 + 22 Windows Terminal From 3996806503bd573aa59a27d1ee8e98bd1d135b8b Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Fri, 3 May 2024 16:08:32 -0500 Subject: [PATCH 48/53] build: switch to TouchdownBuildTask v3, which supports a new type of auth (#17189) --- build/pipelines/daily-loc-submission.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/pipelines/daily-loc-submission.yml b/build/pipelines/daily-loc-submission.yml index 7a7e1769b5..afab55a70f 100644 --- a/build/pipelines/daily-loc-submission.yml +++ b/build/pipelines/daily-loc-submission.yml @@ -51,12 +51,12 @@ steps: git config --local core.autocrlf true displayName: Prepare git submission environment -- task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@1 +- task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@3 displayName: 'Touchdown Build - 7105, PRODEXT' inputs: teamId: 7105 - authId: '$(TouchdownApplicationID)' - authKey: '$(TouchdownApplicationKey)' + TDBuildServiceConnection: $(TouchdownServiceConnection) + authType: SubjectNameIssuer resourceFilePath: | **\en-US\*.resw Terminal.Internal\PDPs\Stable\PDPs\en-us\PDP.xml From b31059e53e23f4d7dabe466cf4c46c08cafb6fba Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 6 May 2024 20:20:40 +0200 Subject: [PATCH 49/53] AtlasEngine: Fix several error handling bugs (#17193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes: * `HRESULT`s not being shown as unsigned hex * `D2DERR_RECREATE_TARGET` not being handled * 4 calls not checking their `HRESULT` return Out of the 4 only `CreateCompatibleRenderTarget` will throw in practice, however it throws `D2DERR_RECREATE_TARGET` which is common. Without this error handling, AtlasEngine may crash. ## Validation Steps Performed * Set Graphics API to Direct2D * Use `DXGIAdapterRemovalSupportTest.exe` to trigger `D2DERR_RECREATE_TARGET` * No error message is shown ✅ * If the `D2DERR_RECREATE_TARGET` handling is removed, the application never crashes due to `cursorRenderTarget` being `nullptr` ✅ --- src/cascadia/TerminalControl/TermControl.cpp | 4 +++- src/renderer/atlas/AtlasEngine.r.cpp | 2 +- src/renderer/atlas/BackendD2D.cpp | 11 ++++++----- src/renderer/atlas/BackendD2D.h | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index e68810d192..df1e15164a 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1120,7 +1120,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation co_return; } - const auto hr = args.Result(); + // HRESULT is a signed 32-bit integer which would result in a hex output like "-0x7766FFF4", + // but canonically HRESULTs are displayed unsigned as "0x8899000C". See GH#11556. + const auto hr = std::bit_cast(args.Result()); const auto parameter = args.Parameter(); winrt::hstring message; diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index f0a448d491..a0dbdcc547 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -55,7 +55,7 @@ catch (const wil::ResultException& exception) { const auto hr = exception.GetErrorCode(); - if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET || hr == D2DERR_RECREATE_TARGET) { _p.dxgi = {}; return E_PENDING; diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 4310bed3f8..d0909de361 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -148,11 +148,11 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) _viewportCellCount = p.s->viewportCellCount; } -void BackendD2D::_drawBackground(const RenderingPayload& p) noexcept +void BackendD2D::_drawBackground(const RenderingPayload& p) { if (_backgroundBitmapGeneration != p.colorBitmapGenerations[0]) { - _backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast(p.colorBitmapRowStride * sizeof(u32))); + THROW_IF_FAILED(_backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast(p.colorBitmapRowStride * sizeof(u32)))); _backgroundBitmapGeneration = p.colorBitmapGenerations[0]; } @@ -356,7 +356,7 @@ f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 _glyphMetrics = Buffer{ size }; } - glyphRun.fontFace->GetDesignGlyphMetrics(glyphRun.glyphIndices, glyphRun.glyphCount, _glyphMetrics.data(), false); + THROW_IF_FAILED(glyphRun.fontFace->GetDesignGlyphMetrics(glyphRun.glyphIndices, glyphRun.glyphCount, _glyphMetrics.data(), false)); const f32 fontScale = glyphRun.fontEmSize / fontMetrics.designUnitsPerEm; f32r accumulatedBounds{ @@ -541,7 +541,8 @@ void BackendD2D::_resizeCursorBitmap(const RenderingPayload& p, const til::size const D2D1_SIZE_F sizeF{ static_cast(newSizeInPx.width), static_cast(newSizeInPx.height) }; const D2D1_SIZE_U sizeU{ gsl::narrow_cast(newSizeInPx.width), gsl::narrow_cast(newSizeInPx.height) }; wil::com_ptr cursorRenderTarget; - _renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, nullptr, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, cursorRenderTarget.addressof()); + THROW_IF_FAILED(_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, nullptr, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, cursorRenderTarget.addressof())); + cursorRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); cursorRenderTarget->BeginDraw(); @@ -553,7 +554,7 @@ void BackendD2D::_resizeCursorBitmap(const RenderingPayload& p, const til::size } THROW_IF_FAILED(cursorRenderTarget->EndDraw()); - cursorRenderTarget->GetBitmap(_cursorBitmap.put()); + THROW_IF_FAILED(cursorRenderTarget->GetBitmap(_cursorBitmap.put())); _cursorBitmapSize = newSize; } diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h index 3bc40bb22c..e6993d6060 100644 --- a/src/renderer/atlas/BackendD2D.h +++ b/src/renderer/atlas/BackendD2D.h @@ -17,7 +17,7 @@ namespace Microsoft::Console::Render::Atlas private: ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p); - void _drawBackground(const RenderingPayload& p) noexcept; + void _drawBackground(const RenderingPayload& p); void _drawText(RenderingPayload& p); ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept; ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept; From 432dfcc4902d1a8393217a5f529d07e65182a3f8 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 6 May 2024 20:18:24 +0100 Subject: [PATCH 50/53] Prevent the VT engine painting unnecessarily (#17194) When the VT render engine starts a paint operation, it first checks to see whether there is actually something to do, and if not it can end the frame early. However, the result of that check was being ignored, which could sometimes result in an unwanted `SGR` reset being written to the conpty pipe. This was particular concerning when passing through `DCS` sequences, because an unexpected `SGR` in the middle of the `DCS` string would cause it to abort early. This PR addresses the problem by making sure the `VtEngine::StartPaint` return value is appropriately handled in the `XtermEngine` class. ## Detailed Description of the Pull Request / Additional comments To make this work, I also needed to correct the `_cursorMoved` flag, because that is one of things that determines whether a paint is needed or not, but it was being set in the `InvalidateCursor` method at the start of ever frame, regardless of whether the cursor had actually moved. I also took this opportunity to get rid of the `_WillWriteSingleChar` method and the `_quickReturn` flag, which have been mostly obsolete for a long time now. The only place the flag was still used was to optimize single char writes when line renditions are active. But that could more easily be handled by testing the `_invalidMap` directly. ## Validation Steps Performed I've confirmed that the test case in issue #17117 is no longer aborting the `DCS` color table sequence early. Closes #17117 --- src/renderer/vt/XtermEngine.cpp | 20 +++++++++---------- src/renderer/vt/invalidate.cpp | 2 +- src/renderer/vt/math.cpp | 35 --------------------------------- src/renderer/vt/paint.cpp | 16 +++++++-------- src/renderer/vt/state.cpp | 1 - src/renderer/vt/vtrenderer.hpp | 3 --- 6 files changed, 19 insertions(+), 58 deletions(-) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 75fc3510fa..b6d0c58a86 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -37,7 +37,16 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // the pipe. [[nodiscard]] HRESULT XtermEngine::StartPaint() noexcept { - RETURN_IF_FAILED(VtEngine::StartPaint()); + const auto hr = VtEngine::StartPaint(); + if (hr != S_OK) + { + // If _noFlushOnEnd was set, and we didn't return early, it would usually + // have been reset in the EndPaint call. But since that's not going to + // happen now, we need to reset it here, otherwise we may mistakenly skip + // the flush on the next frame. + _noFlushOnEnd = false; + return hr; + } _trace.TraceLastText(_lastText); @@ -83,15 +92,6 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, } } - if (!_quickReturn) - { - if (_WillWriteSingleChar()) - { - // Don't re-enable the cursor. - _quickReturn = true; - } - } - return S_OK; } diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index 489a6b50bc..942ba58df2 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -75,7 +75,7 @@ CATCH_RETURN(); } _skipCursor = false; - _cursorMoved = true; + _cursorMoved = psrRegion->origin() != _lastText; return S_OK; } diff --git a/src/renderer/vt/math.cpp b/src/renderer/vt/math.cpp index d86e4644ca..354ca0e992 100644 --- a/src/renderer/vt/math.cpp +++ b/src/renderer/vt/math.cpp @@ -52,38 +52,3 @@ void VtEngine::_OrRect(_Inout_ til::inclusive_rect* const pRectExisting, const t pRectExisting->right = std::max(pRectExisting->right, pRectToOr->right); pRectExisting->bottom = std::max(pRectExisting->bottom, pRectToOr->bottom); } - -// Method Description: -// - Returns true if the invalidated region indicates that we only need to -// simply print text from the current cursor position. This will prevent us -// from sending extra VT set-up/tear down sequences (?12h/l) when all we -// need to do is print more text at the current cursor position. -// Arguments: -// - -// Return Value: -// - true iff only the next character is invalid -bool VtEngine::_WillWriteSingleChar() const -{ - // If there is no scroll delta, return false. - if (til::point{ 0, 0 } != _scrollDelta) - { - return false; - } - - // If there is more than one invalid char, return false. - if (!_invalidMap.one()) - { - return false; - } - - // Get the single point at which things are invalid. - const auto invalidPoint = _invalidMap.runs().front().origin(); - - // Either the next character to the right or the immediately previous - // character should follow this code path - // (The immediate previous character would suggest a backspace) - auto invalidIsNext = invalidPoint == _lastText; - auto invalidIsLast = invalidPoint == til::point{ _lastText.x - 1, _lastText.y }; - - return invalidIsNext || invalidIsLast; -} diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index af08cdd5b5..1fcb533588 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -20,30 +20,30 @@ using namespace Microsoft::Console::Types; // HRESULT error code if painting didn't start successfully. [[nodiscard]] HRESULT VtEngine::StartPaint() noexcept { + // When unit testing, there may be no pipe, but we still need to paint. if (!_hFile) { - return S_FALSE; + return S_OK; } // If we're using line renditions, and this is a full screen paint, we can // potentially stop using them at the end of this frame. _stopUsingLineRenditions = _usingLineRenditions && _AllIsInvalid(); - // If there's nothing to do, quick return + // If there's nothing to do, we won't need to paint. auto somethingToDo = _invalidMap.any() || _scrollDelta != til::point{ 0, 0 } || _cursorMoved || _titleChanged; - _quickReturn = !somethingToDo; - _trace.TraceStartPaint(_quickReturn, + _trace.TraceStartPaint(!somethingToDo, _invalidMap, _lastViewport.ToExclusive(), _scrollDelta, _cursorMoved, _wrappedRow); - return _quickReturn ? S_FALSE : S_OK; + return somethingToDo ? S_OK : S_FALSE; } // Routine Description: @@ -142,9 +142,9 @@ using namespace Microsoft::Console::Types; _usingLineRenditions = true; } // One simple optimization is that we can skip sending the line attributes - // when _quickReturn is true. That indicates that we're writing out a single - // character, which should preclude there being a rendition switch. - if (_usingLineRenditions && !_quickReturn) + // when we're writing out a single character, which should preclude there + // being a rendition switch. + if (_usingLineRenditions && !_invalidMap.one()) { RETURN_IF_FAILED(_MoveCursor({ _lastText.x, targetRow })); switch (lineRendition) diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 881fb0c2cc..f192536e3e 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -37,7 +37,6 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, _pool(til::pmr::get_default_resource()), _invalidMap(initialViewport.Dimensions(), false, &_pool), _scrollDelta(0, 0), - _quickReturn(false), _clearedAllThisFrame(false), _cursorMoved(false), _resized(false), diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 0125fe1736..b5c487fc1c 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -113,7 +113,6 @@ namespace Microsoft::Console::Render til::point _lastText; til::point _scrollDelta; - bool _quickReturn; bool _clearedAllThisFrame; bool _cursorMoved; bool _resized; @@ -214,8 +213,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _RgbUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept; [[nodiscard]] HRESULT _16ColorUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept; - bool _WillWriteSingleChar() const; - // buffer space for these two functions to build their lines // so they don't have to alloc/free in a tight loop std::wstring _bufferLine; From 80d2e5894413ab04ea0ac0b87273751f4160f52e Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Mon, 6 May 2024 14:35:11 -0500 Subject: [PATCH 51/53] Localization Updates - 05/03/2024 19:01:37 (#17188) --- .../TerminalSettingsEditor/Resources/es-ES/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/it-IT/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/ja-JP/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/ko-KR/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/pt-BR/Resources.resw | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw index 06ef4c0b3c..61ef415162 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw @@ -733,10 +733,16 @@ Examinar... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Agregar nuevo eje de fuente + Agregar nuevo Button label that adds a new font axis for the current font. + + Agregar nueva característica de fuente + Agregar nuevo Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw index 6cbfe190b2..1b2550d1d6 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw @@ -733,10 +733,16 @@ Sfoglia... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Aggiungi nuovo asse dei tipi di carattere + Aggiungi nuovo Button label that adds a new font axis for the current font. + + Aggiungi nuova funzionalità dei tipi di carattere + Aggiungi nuovo Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw index 70e4da642b..3ba41206da 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw @@ -733,10 +733,16 @@ 参照... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + 新しいフォント軸の追加 + 新規追加 Button label that adds a new font axis for the current font. + + 新しいフォント機能の追加 + 新規追加 Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw index 04bf305404..4e022bd43b 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw @@ -733,10 +733,16 @@ 찾아보기... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + 새 글꼴 추가 축 + 새로 추가 Button label that adds a new font axis for the current font. + + 새 글꼴 추가 기능 + 새로 추가 Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw index 09611a80f0..8ac46dede9 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw @@ -733,10 +733,16 @@ Procurar... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Adicionar novo eixo de fonte + Adicionar novo Button label that adds a new font axis for the current font. + + Adicionar novo recurso de fonte + Adicionar novo Button label that adds a new font feature for the current font. From 9c16c5ca82943c2c466fb16a093a559d53a2fb0c Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 6 May 2024 21:03:33 +0100 Subject: [PATCH 52/53] Make sure DCS strings are flushed to conpty without delay (#17195) When the `DCS` passthrough code was first implemented, it relied on the `ActionPassThroughString` method flushing the given string immediately. However, that has since stopped being the case, so `DCS` operations end up being delayed until the entire sequence has been parsed. This PR fixes the issue by introducing a `flush` parameter to force an immediate flush on the `ActionPassThroughString` method, as well as the `XtermEngine::WriteTerminalW` method that it calls. ## Validation Steps Performed I've confirmed that the test case in issue #17111 now updates the color table as soon as each color entry is parsed, instead of delaying the updates until the end of the sequence. Closes #17111 --- src/renderer/vt/XtermEngine.cpp | 10 +++++++--- src/renderer/vt/XtermEngine.hpp | 2 +- src/renderer/vt/vtrenderer.hpp | 2 +- src/terminal/adapter/adaptDispatch.cpp | 2 +- src/terminal/parser/IStateMachineEngine.hpp | 2 +- src/terminal/parser/InputStateMachineEngine.cpp | 3 ++- src/terminal/parser/InputStateMachineEngine.hpp | 2 +- src/terminal/parser/OutputStateMachineEngine.cpp | 5 +++-- src/terminal/parser/OutputStateMachineEngine.hpp | 2 +- src/terminal/parser/ut_parser/StateMachineTest.cpp | 2 +- 10 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index b6d0c58a86..9c489aa29c 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -520,9 +520,10 @@ CATCH_RETURN(); // proper utf-8 string, depending on our mode. // Arguments: // - wstr - wstring of text to be written +// - flush - set to true if the string should be flushed immediately // Return Value: // - S_OK or suitable HRESULT error from either conversion or writing pipe. -[[nodiscard]] HRESULT XtermEngine::WriteTerminalW(const std::wstring_view wstr) noexcept +[[nodiscard]] HRESULT XtermEngine::WriteTerminalW(const std::wstring_view wstr, const bool flush) noexcept { RETURN_IF_FAILED(_fUseAsciiOnly ? VtEngine::_WriteTerminalAscii(wstr) : @@ -535,8 +536,11 @@ CATCH_RETURN(); // cause flickering (where we've buffered some state but not the whole // "frame" as specified by the app). We'll just immediately buffer this // sequence, and flush it when the render thread comes around to paint the - // frame normally. - + // frame normally, unless a flush has been explicitly requested. + if (flush) + { + _flushImpl(); + } return S_OK; } diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index 6f1b480b61..7449fb3e38 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -51,7 +51,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT InvalidateScroll(const til::point* const pcoordDelta) noexcept override; - [[nodiscard]] HRESULT WriteTerminalW(const std::wstring_view str) noexcept override; + [[nodiscard]] HRESULT WriteTerminalW(const std::wstring_view str, const bool flush) noexcept override; [[nodiscard]] HRESULT SetWindowVisibility(const bool showOrHide) noexcept override; diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index b5c487fc1c..26fc67cb13 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -78,7 +78,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT RequestCursor() noexcept; [[nodiscard]] HRESULT InheritCursor(const til::point coordCursor) noexcept; [[nodiscard]] HRESULT WriteTerminalUtf8(const std::string_view str) noexcept; - [[nodiscard]] virtual HRESULT WriteTerminalW(const std::wstring_view str) noexcept = 0; + [[nodiscard]] virtual HRESULT WriteTerminalW(const std::wstring_view str, const bool flush = false) noexcept = 0; void SetTerminalOwner(Microsoft::Console::VirtualTerminal::VtIo* const terminalOwner); void SetResizeQuirk(const bool resizeQuirk); void SetLookingForDSRCallback(std::function pfnLooking) noexcept; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index bf038dd75e..02c22db8ee 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -4882,7 +4882,7 @@ ITermDispatch::StringHandler AdaptDispatch::_CreatePassthroughHandler() { buffer += L'\\'; } - engine.ActionPassThroughString(buffer); + engine.ActionPassThroughString(buffer, true); buffer.clear(); } return !endOfString; diff --git a/src/terminal/parser/IStateMachineEngine.hpp b/src/terminal/parser/IStateMachineEngine.hpp index 1eefc5be2a..d1d838ccc5 100644 --- a/src/terminal/parser/IStateMachineEngine.hpp +++ b/src/terminal/parser/IStateMachineEngine.hpp @@ -35,7 +35,7 @@ namespace Microsoft::Console::VirtualTerminal virtual bool ActionPrint(const wchar_t wch) = 0; virtual bool ActionPrintString(const std::wstring_view string) = 0; - virtual bool ActionPassThroughString(const std::wstring_view string) = 0; + virtual bool ActionPassThroughString(const std::wstring_view string, const bool flush = false) = 0; virtual bool ActionEscDispatch(const VTID id) = 0; virtual bool ActionVt52EscDispatch(const VTID id, const VTParameters parameters) = 0; diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp index df7a8c2cfb..4a25d056f4 100644 --- a/src/terminal/parser/InputStateMachineEngine.cpp +++ b/src/terminal/parser/InputStateMachineEngine.cpp @@ -276,9 +276,10 @@ bool InputStateMachineEngine::ActionPrintString(const std::wstring_view string) // string of characters given. // Arguments: // - string - string to dispatch. +// - flush - not applicable to the input state machine. // Return Value: // - true iff we successfully dispatched the sequence. -bool InputStateMachineEngine::ActionPassThroughString(const std::wstring_view string) +bool InputStateMachineEngine::ActionPassThroughString(const std::wstring_view string, const bool /*flush*/) { if (_pDispatch->IsVtInputEnabled()) { diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp index 6b90c72949..4b17669aed 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -142,7 +142,7 @@ namespace Microsoft::Console::VirtualTerminal bool ActionPrintString(const std::wstring_view string) override; - bool ActionPassThroughString(const std::wstring_view string) override; + bool ActionPassThroughString(const std::wstring_view string, const bool flush) override; bool ActionEscDispatch(const VTID id) override; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 34d40fc850..f4b3fcbf67 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -180,14 +180,15 @@ bool OutputStateMachineEngine::ActionPrintString(const std::wstring_view string) // we don't know what to do with it) // Arguments: // - string - string to dispatch. +// - flush - set to true if the string should be flushed immediately. // Return Value: // - true iff we successfully dispatched the sequence. -bool OutputStateMachineEngine::ActionPassThroughString(const std::wstring_view string) +bool OutputStateMachineEngine::ActionPassThroughString(const std::wstring_view string, const bool flush) { auto success = true; if (_pTtyConnection != nullptr) { - const auto hr = _pTtyConnection->WriteTerminalW(string); + const auto hr = _pTtyConnection->WriteTerminalW(string, flush); LOG_IF_FAILED(hr); success = SUCCEEDED(hr); } diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 1ff22ab5a9..64fbf81503 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -38,7 +38,7 @@ namespace Microsoft::Console::VirtualTerminal bool ActionPrintString(const std::wstring_view string) override; - bool ActionPassThroughString(const std::wstring_view string) override; + bool ActionPassThroughString(const std::wstring_view string, const bool flush) override; bool ActionEscDispatch(const VTID id) override; diff --git a/src/terminal/parser/ut_parser/StateMachineTest.cpp b/src/terminal/parser/ut_parser/StateMachineTest.cpp index 0bf36050e9..f910a72c1a 100644 --- a/src/terminal/parser/ut_parser/StateMachineTest.cpp +++ b/src/terminal/parser/ut_parser/StateMachineTest.cpp @@ -59,7 +59,7 @@ public: return true; }; - bool ActionPassThroughString(const std::wstring_view string) override + bool ActionPassThroughString(const std::wstring_view string, const bool /*flush*/) override { passedThrough += string; return true; From 6d0342f0bb31bf245843411c6781d6d5399ff651 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 7 May 2024 20:35:48 +0200 Subject: [PATCH 53/53] Add nullptr checks to shared_ptr conversions (#17199) We use `if (auto self = weakSelf.get())` in a lot of places. That assigns the value to `self` and then checks if it's truthy. Sometimes we need to add a "is (app) closing" check because XAML, so we wrote something akin to `if (self = ...; !closing)`. But that's wrong because the correct `if (foo)` is the same as `if (void; foo)` and not `if (foo; void)` and that meant that we didn't check for `self`'s truthiness anymore. This issue became apparent now, because we added a new kind of delayed callback invocation (which is a lot cheaper). This made the lack of a `nullptr` check finally obvious. --- src/cascadia/TerminalControl/ControlCore.cpp | 4 ++-- src/cascadia/TerminalControl/TermControl.cpp | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index da78e6167a..575864ede1 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -161,7 +161,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::chrono::milliseconds{ 100 }, [weakTerminal = std::weak_ptr{ _terminal }, weakThis = get_weak(), dispatcher = _dispatcher]() { dispatcher.TryEnqueue(DispatcherQueuePriority::Normal, [weakThis]() { - if (const auto self = weakThis.get(); !self->_IsClosing()) + if (const auto self = weakThis.get(); self && !self->_IsClosing()) { self->OutputIdle.raise(*self, nullptr); } @@ -179,7 +179,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _dispatcher, std::chrono::milliseconds{ 8 }, [weakThis = get_weak()](const auto& update) { - if (auto core{ weakThis.get() }; !core->_IsClosing()) + if (auto core{ weakThis.get() }; core && !core->_IsClosing()) { core->ScrollPositionChanged.raise(*core, update); } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index df1e15164a..c1c7cc73e5 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -248,7 +248,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation dispatcher, TerminalWarningBellInterval, [weakThis = get_weak()]() { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { control->WarningBell.raise(*control, nullptr); } @@ -258,7 +258,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation dispatcher, ScrollBarUpdateInterval, [weakThis = get_weak()](const auto& update) { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { control->_throttledUpdateScrollbar(update); } @@ -301,7 +301,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _originalSelectedSecondaryElements.Append(e); } ContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { const auto& menu{ control->ContextMenu() }; menu.PrimaryCommands().Clear(); @@ -317,7 +317,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } }); SelectionContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { const auto& menu{ control->SelectionContextMenu() }; menu.PrimaryCommands().Clear(); @@ -544,7 +544,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } _searchBox->Open([weakThis = get_weak()]() { - if (const auto self = weakThis.get(); !self->_IsClosing()) + if (const auto self = weakThis.get(); self && !self->_IsClosing()) { self->_searchBox->SetFocusOnTextbox(); self->_refreshSearch();