diff --git a/src/GitHub.UI/Controls/ScrollingVerticalStackPanel.cs b/src/GitHub.UI/Controls/ScrollingVerticalStackPanel.cs
new file mode 100644
index 0000000000..5e423ba8c1
--- /dev/null
+++ b/src/GitHub.UI/Controls/ScrollingVerticalStackPanel.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+
+namespace GitHub.UI.Controls
+{
+ ///
+ /// A vertical stack panel which implements its own logical scrolling, allowing controls to be
+ /// fixed horizontally in the scroll area.
+ ///
+ ///
+ /// This panel is needed by the PullRequestDetailsView because of #1698: there is no default
+ /// panel in WPF which allows the horizontal scrollbar to always be present at the bottom while
+ /// also making the PR description etc be fixed horizontally (non-scrollable) in the viewport.
+ ///
+ public class ScrollingVerticalStackPanel : Panel, IScrollInfo
+ {
+ const int lineSize = 16;
+ const int mouseWheelSize = 48;
+
+ ///
+ /// Attached property which when set to True on a child control, will cause it to be fixed
+ /// horizontally within the scrollable viewport.
+ ///
+ public static readonly DependencyProperty IsFixedProperty =
+ DependencyProperty.RegisterAttached(
+ "IsFixed",
+ typeof(bool),
+ typeof(ScrollingVerticalStackPanel),
+ new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public bool CanHorizontallyScroll
+ {
+ get { return true; }
+ set { }
+ }
+
+ public bool CanVerticallyScroll
+ {
+ get { return true; }
+ set { }
+ }
+
+ public double ExtentHeight { get; private set; }
+ public double ExtentWidth { get; private set; }
+ public double HorizontalOffset { get; private set; }
+ public double VerticalOffset { get; private set; }
+ public double ViewportHeight { get; private set; }
+ public double ViewportWidth { get; private set; }
+ public ScrollViewer ScrollOwner { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Can only be applied to controls")]
+ public static bool GetIsFixed(FrameworkElement control)
+ {
+ return (bool)control.GetValue(IsFixedProperty);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Can only be applied to controls")]
+ public static void SetIsFixed(FrameworkElement control, bool value)
+ {
+ control.SetValue(IsFixedProperty, value);
+ }
+
+ public void LineDown() => SetVerticalOffset(VerticalOffset + lineSize);
+ public void LineLeft() => SetHorizontalOffset(HorizontalOffset - lineSize);
+ public void LineRight() => SetHorizontalOffset(HorizontalOffset + lineSize);
+ public void LineUp() => SetVerticalOffset(VerticalOffset - lineSize);
+ public void MouseWheelDown() => SetVerticalOffset(VerticalOffset + mouseWheelSize);
+ public void MouseWheelLeft() => SetHorizontalOffset(HorizontalOffset - mouseWheelSize);
+ public void MouseWheelRight() => SetHorizontalOffset(HorizontalOffset + mouseWheelSize);
+ public void MouseWheelUp() => SetVerticalOffset(VerticalOffset - mouseWheelSize);
+ public void PageDown() => SetVerticalOffset(VerticalOffset + ViewportHeight);
+ public void PageLeft() => SetHorizontalOffset(HorizontalOffset - ViewportWidth);
+ public void PageRight() => SetHorizontalOffset(HorizontalOffset + ViewportWidth);
+ public void PageUp() => SetVerticalOffset(VerticalOffset - ViewportHeight);
+
+ public Rect MakeVisible(Visual visual, Rect rectangle)
+ {
+ var transform = visual.TransformToVisual(this);
+ var rect = transform.TransformBounds(rectangle);
+ var offsetX = HorizontalOffset;
+ var offsetY = VerticalOffset;
+
+ if (rect.Bottom > ViewportHeight)
+ {
+ var delta = rect.Bottom - ViewportHeight;
+ offsetY += delta;
+ rect.Y -= delta;
+ }
+
+ if (rect.Y < 0)
+ {
+ offsetY += rect.Y;
+ }
+
+ // We technially should be trying to also show the right-hand side of the rect here
+ // using the same technique that we just used to show the bottom of the rect above,
+ // but in the case of the PR details view, the left hand side of the item is much
+ // more important than the right hand side and it actually feels better to not do
+ // this. If this control is used elsewhere and this behavior is required, we could
+ // put in a switch to enable it.
+
+ if (rect.X < 0)
+ {
+ offsetX += rect.X;
+ }
+
+ SetHorizontalOffset(offsetX);
+ SetVerticalOffset(offsetY);
+
+ return rect;
+ }
+
+ public void SetHorizontalOffset(double offset)
+ {
+ var value = Math.Max(0, Math.Min(offset, ExtentWidth - ViewportWidth));
+
+ if (value != HorizontalOffset)
+ {
+ HorizontalOffset = value;
+ InvalidateArrange();
+ }
+ }
+
+ public void SetVerticalOffset(double offset)
+ {
+ var value = Math.Max(0, Math.Min(offset, ExtentHeight - ViewportHeight));
+
+ if (value != VerticalOffset)
+ {
+ VerticalOffset = value;
+ InvalidateArrange();
+ }
+ }
+
+ protected override void ParentLayoutInvalidated(UIElement child)
+ {
+ base.ParentLayoutInvalidated(child);
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var maxWidth = 0.0;
+ var height = 0.0;
+
+ foreach (FrameworkElement child in Children)
+ {
+ var isFixed = GetIsFixed(child);
+ var childConstraint = new Size(
+ isFixed ? availableSize.Width : double.PositiveInfinity,
+ double.PositiveInfinity);
+ child.Measure(childConstraint);
+
+ if (height - VerticalOffset < availableSize.Height)
+ {
+ maxWidth = Math.Max(maxWidth, child.DesiredSize.Width);
+ }
+
+ height += child.DesiredSize.Height;
+ }
+
+ UpdateScrollInfo(new Size(maxWidth, height), availableSize);
+
+ return new Size(
+ Math.Min(maxWidth, availableSize.Width),
+ Math.Min(height, availableSize.Height));
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var y = -VerticalOffset;
+ var thisRect = new Rect(finalSize);
+ var visibleMaxWidth = 0.0;
+
+ foreach (FrameworkElement child in Children)
+ {
+ var isFixed = GetIsFixed(child);
+ var x = isFixed ? 0 : -HorizontalOffset;
+ var childRect = new Rect(x, y, child.DesiredSize.Width, child.DesiredSize.Height);
+ child.Arrange(childRect);
+ y += child.DesiredSize.Height;
+
+ if (childRect.IntersectsWith(thisRect) && childRect.Right > visibleMaxWidth)
+ {
+ visibleMaxWidth = childRect.Right;
+ }
+ }
+
+ UpdateScrollInfo(new Size(visibleMaxWidth, ExtentHeight), new Size(finalSize.Width, finalSize.Height));
+ return finalSize;
+ }
+
+ void UpdateScrollInfo(Size extent, Size viewport)
+ {
+ ExtentWidth = extent.Width;
+ ExtentHeight = extent.Height;
+ ScrollOwner?.InvalidateScrollInfo();
+ ViewportWidth = viewport.Width;
+ ViewportHeight = viewport.Height;
+ ScrollOwner?.InvalidateScrollInfo();
+ }
+ }
+}
diff --git a/src/GitHub.UI/GitHub.UI.csproj b/src/GitHub.UI/GitHub.UI.csproj
index b5e6c4b839..ae7d07f227 100644
--- a/src/GitHub.UI/GitHub.UI.csproj
+++ b/src/GitHub.UI/GitHub.UI.csproj
@@ -85,6 +85,7 @@
True
OcticonPaths.resx
+
Spinner.xaml
diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml
index 85419b6c70..67f493c3a5 100644
--- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml
+++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml
@@ -162,26 +162,15 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
View on GitHub
@@ -255,9 +244,9 @@
+ HeaderText="Description"
+ IsExpanded="True"
+ ghfvs:ScrollingVerticalStackPanel.IsFixed="true">
@@ -282,7 +271,7 @@
HeaderText="Reviewers"
IsExpanded="True"
Margin="0 8 0 0"
- Grid.Row="2">
+ ghfvs:ScrollingVerticalStackPanel.IsFixed="true">
@@ -294,13 +283,17 @@
-
-
-
+ Grid.Row="4"
+ IsExpanded="True"
+ HeaderText="{Binding Files.ChangedFilesCount, StringFormat={x:Static prop:Resources.ChangesCountFormat}}"
+ Margin="0 8 10 0"
+ ghfvs:ScrollingVerticalStackPanel.IsFixed="true"/>
+
+
+
+
\ No newline at end of file
diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs
index 2f2956ef45..9b87f84b8c 100644
--- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs
+++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs
@@ -7,6 +7,7 @@
using GitHub.Exports;
using GitHub.UI.Helpers;
using GitHub.ViewModels.GitHubPane;
+using GitHub.VisualStudio.UI.Helpers;
namespace GitHub.VisualStudio.Views.GitHubPane
{
@@ -17,6 +18,7 @@ public partial class PullRequestFilesView : UserControl
public PullRequestFilesView()
{
InitializeComponent();
+ PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll;
}
protected override void OnMouseDown(MouseButtonEventArgs e)