Repository: microsoft/terminal (language: cpp)

Issue:
Scrollbar too slow to show, grab
# Environment

```none
Platform ServicePack Version      VersionString
-------- ----------- -------      -------------
 Win32NT             10.0.18363.0 Microsoft Windows NT 10.0.18363.0

Terminal version: 0.6.2951.0
```

# Problem (no steps)

Even with the default visibility of the scrollbar, it's too small to grab and the full size doesn't show fast enough - it's a productivity drain. Using my scroll wheel on my mouse is great when I have to scroll a few pages, but when I need to go back further in the output buffer I want to grab the scroll bar. Now, I have to wait a second or so before I can. Please give us a setting to always show it, like "hidden", "visible", or (new) "always" (or something like that). I want it always fully visible - at full size - ready to use like conhost.
Scrollbar too slow to show, grab
# Environment

```none
Platform ServicePack Version      VersionString
-------- ----------- -------      -------------
 Win32NT             10.0.18363.0 Microsoft Windows NT 10.0.18363.0

Terminal version: 0.6.2951.0
```

# Problem (no steps)

Even with the default visibility of the scrollbar, it's too small to grab and the full size doesn't show fast enough - it's a productivity drain. Using my scroll wheel on my mouse is great when I have to scroll a few pages, but when I need to go back further in the output buffer I want to grab the scroll bar. Now, I have to wait a second or so before I can. Please give us a setting to always show it, like "hidden", "visible", or (new) "always" (or something like that). I want it always fully visible - at full size - ready to use like conhost.


Discussion / hints:
You know, I really want the scrollbar to have these options too. Actually getting the XAML scrollbar to behave that way was a lot tricker than we had originally hoped, but maybe we can still get this in.

To clarify: this proposal is to have the scrollbar have three visibility states:

| `scrollbarState`    | Description                                                 |
|---------------------|-------------------------------------------------------------|
| "always"            | The scrollbar is always visible, like conhost.exe           |
| "visible" (default) | The scrollbar "reveals" on mouseover, as it does currently. |
| "hidden"            | The scrollbar is always hidden                              |

Right now we support "visible" and "hidden", but we want an "always" state added.

If someone with more XAML experience would be willing to help us with this one, that'd be much appreciated.

\<technical details\>

From a internal mail thread back in April:

> Mike is partially correct. On windows versions where conscious scroll bars are available (RS3+) the TouchIndicator state is nearly equivalent to the mouse indicator state. The only difference is that when conscious scroll bars are disabled by windows system setting, scrollbars in the touch indicator state are not hit test visible, meaning you cannot drag the thumb to scroll.  On older versions of the OS MouseIndicator and TouchIndicator behave quite differently, essentially how mike described it.
>
> Unfortunately as far as I am aware, there is no easy way to force the scroll bar to remain in its expanded state for your app, if that is desired I think your only option would be to retemplate your scrollbar control and edit the VisualStates for the ConsciousStates group so that the Collapsed and CollapsedWithoutAnimation are eqivalent to the expanded states and to remove the Expanded to Collapsed transition.

So we'll need to basically make a custom scrollbar template for the TermControl scrollbars. Not _impossible_, but certainly not easy.

As another fun challenge, the TerminalControl project doesn't currently have _any_ XAML files or resources, so we'd need to add those, or find a way to define them in TerminalApp and have them used by TermControl.

<hr> 

**EDIT**: July 2020 update

This will be substantially easier to do nowadays. As of late 2019, the TerminalControl project has XAML resources as well, so adding the appropriate XAML files shouldn't be that big of a pain.
Can you push back on the XAML team or the OS team or somebody to make this behavior more accessible? I want always-visible scrollbars in the terminal, but I also want them in all programs that I use.

Always visible-scrollbars are much faster to grab and use (as the original bug filter said), and they also give a visual indication of how big your document is, and where you are in it. And, I can't see any advantages to having them disappear - the disappearing doesn't give you more window space for text.
The current XAML/WPF/whatever scrollbar thumb is also just way too small. My vision is good so I can see it but it's low-contrast and only about 2 pixels wide on both of my monitors. If it's going to collapse it should not be doing it in such a low-accessibility way.
@randomascii If you want to make the scrollbar always visible for all programs, there is a setting under Ease of Access->Display, namely "Automatically hide scroll bars in Windows". You can just search and disable it.
@randomascii Probably the best place to push for that would be filing an issue over in the [WinUI repo](https://github.com/microsoft/microsoft-ui-xaml/issues). 

We'd definitely be willing to back up a request, though knowing what I know about their resourcing currently, it'd almost certainly be easier for us to add a custom templated scrollbar than for them to add the behavior we're looking for to the platform. This is at least in the near term - sure, the platform fix would be better for the ecosystem, but if you want a fix _now_ for the terminal, doing it here is going to get you the fastest results.

Fortunately, quite a bit has changed since November, and we've got quite a bit more XAML in our project than we used to. So I actually don't think that the changes that were outlined above would be terribly difficult to implement. I'd think the hardest part would be supporting both the current behavior and the requested behavior in _one_ control class. It would probably be really easy to just have two control classes, and just instantiate one or the other. In fact, if we were able to get it figured out _here_, we'd probably be able to ingest the changes upstream in WinUI. 
> If you want to make the scrollbar always visible for all programs

Done
When using Telnet Client (feature built in by windows 10) on windows terminal the scrollbar does not work correctly, please look into it.
in CMD it works fine.
this is my issue which was closed. #6044 
thanks.
Your telnet client has implemented scrolling wrong. Your feature request sounded like "make the scroll bar always visible", but you just indicated that it was actually a bug report where "the scroll bar doesn't work for telnet". That's a completely different thing.
> @randomascii If you want to make the scrollbar always visible for all programs, there is a setting under Ease of Access->Display, namely "Automatically hide scroll bars in Windows". You can just search and disable it.

This works, but it seems to only take effect for new instances of Windows Terminal.

I think that existing instances of Windows Terminal should immediately respond when the value of this system-wide setting changes.   And perhaps the title of this issue should be adjusted to reflect how the discussion has evolved?

I would really like to be able to configure this per-profile, as per the idea above.  But I've found that only "visible" and "hidden" are valid values.  I'd like the ability to specify "always" for certain profiles, regardless of the value of the platform wide "Automatically hide scroll bars in Windows" setting in Ease of Access.

> > @randomascii If you want to make the scrollbar always visible for all programs, there is a setting under Ease of Access->Display, namely "Automatically hide scroll bars in Windows". You can just search and disable it.
> 
> This works, but it seems to only take effect for new instances of Windows Terminal.

It looks like that was fixed, and the Accessibility option is effective immediately and on existing windows now.

You know, I really want the scrollbar to have these options too. Actually getting the XAML scrollbar to behave that way was a lot tricker than we had originally hoped, but maybe we can still get this in.

To clarify: this proposal is to have the scrollbar have three visibility states:

| `scrollbarState`    | Description                                                 |
|---------------------|-------------------------------------------------------------|
| "always"            | The scrollbar is always visible, like conhost.exe           |
| "visible" (default) | The scrollbar "reveals" on mouseover, as it does currently. |
| "hidden"            | The scrollbar is always hidden                              |

Right now we support "visible" and "hidden", but we want an "always" state added.

If someone with more XAML experience would be willing to help us with this one, that'd be much appreciated.

\<technical details\>

From a internal mail thread back in April:

> Mike is partially correct. On windows versions where conscious scroll bars are available (RS3+) the TouchIndicator state is nearly equivalent to the mouse indicator state. The only difference is that when conscious scroll bars are disabled by windows system setting, scrollbars in the touch indicator state are not hit test visible, meaning you cannot drag the thumb to scroll.  On older versions of the OS MouseIndicator and TouchIndicator behave quite differently, essentially how mike described it.
>
> Unfortunately as far as I am aware, there is no easy way to force the scroll bar to remain in its expanded state for your app, if that is desired I think your only option would be to retemplate your scrollbar control and edit the VisualStates for the ConsciousStates group so that the Collapsed and CollapsedWithoutAnimation are eqivalent to the expanded states and to remove the Expanded to Collapsed transition.

So we'll need to basically make a custom scrollbar template for the TermControl scrollbars. Not _impossible_, but certainly not easy.

As another fun challenge, the TerminalControl project doesn't currently have _any_ XAML files or resources, so we'd need to add those, or find a way to define them in TerminalApp and have them used by TermControl.

<hr> 

**EDIT**: July 2020 update

This will be substantially easier to do nowadays. As of late 2019, the TerminalControl project has XAML resources as well, so adding the appropriate XAML files shouldn't be that big of a pain.
Can you push back on the XAML team or the OS team or somebody to make this behavior more accessible? I want always-visible scrollbars in the terminal, but I also want them in all programs that I use.

Always visible-scrollbars are much faster to grab and use (as the original bug filter said), and they also give a visual indication of how big your document is, and where you are in it. And, I can't see any advantages to having them disappear - the disappearing doesn't give you more window space for text.
The current XAML/WPF/whatever scrollbar thumb is also just way too small. My vision is good so I can see it but it's low-contrast and only about 2 pixels wide on both of my monitors. If it's going to collapse it should not be doing it in such a low-accessibility way.
@randomascii If you want to make the scrollbar always visible for all programs, there is a setting under Ease of Access->Display, namely "Automatically hide scroll bars in Windows". You can just search and disable it.
@randomascii Probably the best place to push for that would be filing an issue over in the [WinUI repo](https://github.com/microsoft/microsoft-ui-xaml/issues). 

We'd definitely be willing to back up a request, though knowing what I know about their resourcing currently, it'd almost certainly be easier for us to add a custom templated scrollbar than for them to add the behavior we're looking for to the platform. This is at least in the near term - sure, the platform fix would be better for the ecosystem, but if you want a fix _now_ for the terminal, doing it here is going to get you the fastest results.

Fortunately, quite a bit has changed since November, and we've got quite a bit more XAML in our project than we used to. So I actually don't think that the changes that were outlined above would be terribly difficult to implement. I'd think the hardest part would be supporting both the current behavior and the requested behavior in _one_ control class. It would probably be really easy to just have two control classes, and just instantiate one or the other. In fact, if we were able to get it figured out _here_, we'd probably be able to ingest the changes upstream in WinUI. 
> If you want to make the scrollbar always visible for all programs

Done
When using Telnet Client (feature built in by windows 10) on windows terminal the scrollbar does not work correctly, please look into it.
in CMD it works fine.
this is my issue which was closed. #6044 
thanks.
Your telnet client has implemented scrolling wrong. Your feature request sounded like "make the scroll bar always visible", but you just indicated that it was actually a bug report where "the scroll bar doesn't work for telnet". That's a completely different thing.
> @randomascii If you want to make the scrollbar always visible for all programs, there is a setting under Ease of Access->Display, namely "Automatically hide scroll bars in Windows". You can just search and disable it.

This works, but it seems to only take effect for new instances of Windows Terminal.

I think that existing instances of Windows Terminal should immediately respond when the value of this system-wide setting changes.   And perhaps the title of this issue should be adjusted to reflect how the discussion has evolved?

I would really like to be able to configure this per-profile, as per the idea above.  But I've found that only "visible" and "hidden" are valid values.  I'd like the ability to specify "always" for certain profiles, regardless of the value of the platform wide "Automatically hide scroll bars in Windows" setting in Ease of Access.

> > @randomascii If you want to make the scrollbar always visible for all programs, there is a setting under Ease of Access->Display, namely "Automatically hide scroll bars in Windows". You can just search and disable it.
> 
> This works, but it seems to only take effect for new instances of Windows Terminal.

It looks like that was fixed, and the Accessibility option is effective immediately and on existing windows now.


Fix patch:
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index 19742372e2d..d29a73cc603 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -2350,7 +2350,8 @@
           "description": "Defines the visibility of the scrollbar.",
           "enum": [
             "visible",
-            "hidden"
+            "hidden",
+            "always"
           ],
           "type": "string"
         },
diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl
index a7bfcf255a3..d014b8ac9be 100644
--- a/src/cascadia/TerminalControl/IControlSettings.idl
+++ b/src/cascadia/TerminalControl/IControlSettings.idl
@@ -9,7 +9,8 @@ namespace Microsoft.Terminal.Control
     enum ScrollbarState
     {
         Visible = 0,
-        Hidden
+        Hidden,
+        Always
     };
 
     enum TextAntialiasingMode
diff --git a/src/cascadia/TerminalControl/ScrollBarVisualStateManager.cpp b/src/cascadia/TerminalControl/ScrollBarVisualStateManager.cpp
new file mode 100644
index 00000000000..b42a853df80
--- /dev/null
+++ b/src/cascadia/TerminalControl/ScrollBarVisualStateManager.cpp
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ScrollBarVisualStateManager.h"
+#include "ScrollBarVisualStateManager.g.cpp"
+
+namespace winrt::Microsoft::Terminal::Control::implementation
+{
+    ScrollBarVisualStateManager::ScrollBarVisualStateManager() :
+        _termControl(nullptr)
+    {
+    }
+
+    bool ScrollBarVisualStateManager::GoToStateCore(
+        winrt::Windows::UI::Xaml::Controls::Control const& control,
+        winrt::Windows::UI::Xaml::FrameworkElement const& templateRoot,
+        winrt::hstring const& stateName,
+        winrt::Windows::UI::Xaml::VisualStateGroup const& group,
+        winrt::Windows::UI::Xaml::VisualState const& state,
+        bool useTransitions)
+    {
+        if (!_termControl)
+        {
+            for (auto parent = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(control);
+                 parent != nullptr;
+                 parent = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(parent))
+            {
+                if (parent.try_as(_termControl))
+                {
+                    break;
+                }
+            }
+        }
+
+        WINRT_ASSERT(_termControl);
+
+        auto scrollState = _termControl.Settings().ScrollState();
+        if (scrollState == ScrollbarState::Always)
+        {
+            // If we're in Always mode, and the control is trying to collapse,
+            // go back to expanded
+            if (stateName == L"Collapsed" || stateName == L"CollapsedWithoutAnimation")
+            {
+                for (auto foundState : group.States())
+                {
+                    if (foundState.Name() == L"ExpandedWithoutAnimation")
+                    {
+                        return base_type::GoToStateCore(control, templateRoot, foundState.Name(), group, foundState, false);
+                    }
+                }
+            }
+        }
+
+        return base_type::GoToStateCore(control, templateRoot, stateName, group, state, useTransitions);
+    }
+}
diff --git a/src/cascadia/TerminalControl/ScrollBarVisualStateManager.h b/src/cascadia/TerminalControl/ScrollBarVisualStateManager.h
new file mode 100644
index 00000000000..98c3c2bff9c
--- /dev/null
+++ b/src/cascadia/TerminalControl/ScrollBarVisualStateManager.h
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+// Module Name:
+// - ScrollBarVisualStateManager.h
+//
+// Abstract:
+// - This is a custom VisualStateManager that keeps the scroll bar from
+//   going into the collapsed mode when the user wants the scroll bar
+//   to always be displayed.
+//
+
+#pragma once
+#include "winrt/Windows.UI.Xaml.h"
+#include "winrt/Windows.UI.Xaml.Controls.h"
+
+#include "ScrollBarVisualStateManager.g.h"
+
+namespace winrt::Microsoft::Terminal::Control::implementation
+{
+    struct ScrollBarVisualStateManager : ScrollBarVisualStateManagerT<ScrollBarVisualStateManager>
+    {
+        ScrollBarVisualStateManager();
+
+        bool GoToStateCore(winrt::Windows::UI::Xaml::Controls::Control const& control, winrt::Windows::UI::Xaml::FrameworkElement const& templateRoot, hstring const& stateName, winrt::Windows::UI::Xaml::VisualStateGroup const& group, winrt::Windows::UI::Xaml::VisualState const& state, bool useTransitions);
+
+    private:
+        winrt::Microsoft::Terminal::Control::TermControl _termControl;
+    };
+}
+
+namespace winrt::Microsoft::Terminal::Control::factory_implementation
+{
+    struct ScrollBarVisualStateManager : ScrollBarVisualStateManagerT<ScrollBarVisualStateManager, implementation::ScrollBarVisualStateManager>
+    {
+    };
+}
diff --git a/src/cascadia/TerminalControl/ScrollBarVisualStateManager.idl b/src/cascadia/TerminalControl/ScrollBarVisualStateManager.idl
new file mode 100644
index 00000000000..775db8e3984
--- /dev/null
+++ b/src/cascadia/TerminalControl/ScrollBarVisualStateManager.idl
@@ -0,0 +1,8 @@
+namespace Microsoft.Terminal.Control
+{
+    [default_interface]
+    runtimeclass ScrollBarVisualStateManager : Windows.UI.Xaml.VisualStateManager
+    {
+        ScrollBarVisualStateManager();
+    }
+}
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index a965f35ff8c..5b0e953e18a 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -2117,7 +2117,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         double width = cols * actualFontSize.X;
 
         // Reserve additional space if scrollbar is intended to be visible
-        if (scrollState == ScrollbarState::Visible)
+        if (scrollState != ScrollbarState::Hidden)
         {
             width += scrollbarSize;
         }
@@ -2161,7 +2161,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             double width = fontSize.Width;
             double height = fontSize.Height;
             // Reserve additional space if scrollbar is intended to be visible
-            if (_core.Settings().ScrollState() == ScrollbarState::Visible)
+            if (_core.Settings().ScrollState() != ScrollbarState::Hidden)
             {
                 width += ScrollBar().ActualWidth();
             }
@@ -2204,7 +2204,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                                            padding.Left + padding.Right :
                                                            padding.Top + padding.Bottom);
 
-        if (widthOrHeight && _core.Settings().ScrollState() == ScrollbarState::Visible)
+        if (widthOrHeight && _core.Settings().ScrollState() != ScrollbarState::Hidden)
         {
             nonTerminalArea += gsl::narrow_cast<float>(ScrollBar().ActualWidth());
         }
diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml
index 96c9c575895..1e2428eba3b 100644
--- a/src/cascadia/TerminalControl/TermControl.xaml
+++ b/src/cascadia/TerminalControl/TermControl.xaml
@@ -408,6 +408,11 @@
                                     </Rectangle>
                                 </ControlTemplate>
                             </Grid.Resources>
+
+                            <VisualStateManager.CustomVisualStateManager>
+                                <local:ScrollBarVisualStateManager />
+                            </VisualStateManager.CustomVisualStateManager>
+
                             <Grid x:Name="HorizontalRoot"
                                   contract7Present:CornerRadius="{TemplateBinding CornerRadius}"
                                   Background="{TemplateBinding Background}"
diff --git a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
index 56c60bee500..848f79ddf01 100644
--- a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
+++ b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
@@ -30,7 +30,7 @@
   <Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
   <Import Project="$(OpenConsoleDir)src\common.nugetversions.props" />
   <Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
-
+  
   <!-- ========================= Headers ======================== -->
   <ItemGroup>
     <ClInclude Include="pch.h" />
@@ -40,6 +40,9 @@
     <ClInclude Include="ControlInteractivity.h">
       <DependentUpon>ControlInteractivity.idl</DependentUpon>
     </ClInclude>
+    <ClInclude Include="ScrollBarVisualStateManager.h">
+      <DependentUpon>ScrollBarVisualStateManager.idl</DependentUpon>
+    </ClInclude>
     <ClInclude Include="KeyChord.h">
       <DependentUpon>KeyChord.idl</DependentUpon>
     </ClInclude>
@@ -77,6 +80,9 @@
     <ClCompile Include="ControlInteractivity.cpp">
       <DependentUpon>ControlInteractivity.idl</DependentUpon>
     </ClCompile>
+    <ClCompile Include="ScrollBarVisualStateManager.cpp">
+      <DependentUpon>ScrollBarVisualStateManager.idl</DependentUpon>
+    </ClCompile>
     <ClCompile Include="EventArgs.cpp">
       <DependentUpon>EventArgs.idl</DependentUpon>
     </ClCompile>
@@ -109,6 +115,7 @@
   <ItemGroup>
     <Midl Include="ControlCore.idl" />
     <Midl Include="ControlInteractivity.idl" />
+    <Midl Include="ScrollBarVisualStateManager.idl" />
     <Midl Include="ICoreState.idl" />
     <Midl Include="IDirectKeyListener.idl" />
     <Midl Include="KeyChord.idl" />
diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
index dbd63e0f3f1..b257eb68aaf 100644
--- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
@@ -936,7 +936,11 @@
   </data>
   <data name="Profile_ScrollbarVisibilityVisible.Content" xml:space="preserve">
     <value>Visible</value>
-    <comment>An option to choose from for the "scrollbar visibility" setting. When selected, the scrollbar is visible.</comment>
+    <comment>An option to choose from for the "scrollbar visibility" setting. When selected, the scrollbar is visible but may dynamically shrink.</comment>
+  </data>
+  <data name="Profile_ScrollbarVisibilityAlways.Content" xml:space="preserve">
+    <value>Always</value>
+    <comment>An option to choose from for the "scrollbar visibility" setting. When selected, the scrollbar is always visible.</comment>
   </data>
   <data name="Profile_SnapOnInput.Header" xml:space="preserve">
     <value>Scroll to input when typing</value>
@@ -1505,4 +1509,4 @@
     <value>Set as default</value>
     <comment>Text label for a button that, when invoked, sets the selected color scheme as the default scheme to use.</comment>
   </data>
-</root>
\ No newline at end of file
+</root>
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
index a26c0b7e73d..1f806423b45 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
@@ -70,9 +70,10 @@ JSON_ENUM_MAPPER(::winrt::Windows::UI::Xaml::Media::Stretch)
 
 JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollbarState)
 {
-    static constexpr std::array<pair_type, 2> mappings = {
+    static constexpr std::array<pair_type, 3> mappings = {
         pair_type{ "visible", ValueType::Visible },
-        pair_type{ "hidden", ValueType::Hidden }
+        pair_type{ "hidden", ValueType::Hidden },
+        pair_type{ "always", ValueType::Always }
     };
 };
 


Test patch: