diff --git a/ILSpy/Controls/TreeView/SharpTreeView.cs b/ILSpy/Controls/TreeView/SharpTreeView.cs
index f148f6f9f..e126e9db1 100644
--- a/ILSpy/Controls/TreeView/SharpTreeView.cs
+++ b/ILSpy/Controls/TreeView/SharpTreeView.cs
@@ -197,8 +197,8 @@ namespace ICSharpCode.ILSpy.Controls.TreeView
}
if (lastVisibleChild != node)
{
- ScrollIntoView(lastVisibleChild);
- Dispatcher.UIThread.Post(() => ScrollIntoView(node), DispatcherPriority.Loaded);
+ ScrollRowIntoView(lastVisibleChild, centre: false);
+ Dispatcher.UIThread.Post(() => ScrollRowIntoView(node, centre: false), DispatcherPriority.Loaded);
}
}
@@ -251,48 +251,100 @@ namespace ICSharpCode.ILSpy.Controls.TreeView
///
/// Reveals centred in the viewport so the user's eye lands on a
- /// newly selected row, rather than at the nearest edge (where
- /// leaves it). Skips the move when the row is already fully visible, so clicking a visible
- /// row -- or selecting a freshly-loaded top-level entry that's already on screen -- never
- /// yanks the viewport.
+ /// newly selected row, rather than at the nearest edge. Skips the move when the row is
+ /// already roughly centred, so re-selecting it never twitches the viewport. We
+ /// deliberately do NOT skip a merely-visible row sitting at an edge: the ListBox's
+ /// AutoScrollToSelectedItem drags the selected row to the nearest edge first, and a
+ /// reveal should still pull it to the centre from there. (Skipping an already-visible
+ /// row is decided one level up, before AutoScroll runs, in the model->tree sync --
+ /// see TreeSelectionBinder.SyncModelToTree.)
///
- void CenterNodeInView(SharpTreeNode node)
+ void CenterNodeInView(SharpTreeNode node) => ScrollRowIntoView(node, centre: true);
+
+ ///
+ /// Hides so the obvious way to reveal a
+ /// row is also the safe one -- see for what the base
+ /// method does to a container it realises. Hiding is not enforcement: a call through an
+ /// -typed reference still reaches the base method. It is here
+ /// so that code written later, on a SharpTreeView-typed variable, lands on the safe path
+ /// without its author having to know any of this.
+ ///
+ public new void ScrollIntoView(object item)
{
- var scrollViewer = this.GetVisualDescendants().OfType().FirstOrDefault();
- if (scrollViewer is null)
- {
- ScrollIntoView(node);
+ if (item is SharpTreeNode node)
+ ScrollRowIntoView(node, centre: false);
+ }
+
+ ///
+ public new void ScrollIntoView(int index)
+ {
+ if (flattener is { } rows && (uint)index < (uint)rows.Count)
+ ScrollIntoView(rows[index]);
+ }
+
+ ///
+ /// Scrolls to a row by setting the offset the row's index implies, rather than calling
+ /// .
+ ///
+ /// That method realises a container for the target, arranges it at its own desired
+ /// width, parks it aside for the duration of a few layout passes and then drops the
+ /// reference. A container the passes do not adopt back into the realized range is left
+ /// a visible child of the panel that nothing arranges again -- it keeps painting its
+ /// old item at its old position, over whatever row now occupies that spot. Rows here
+ /// are a uniform height, so the arithmetic below reaches the same place without ever
+ /// entering that code path.
+ ///
+ void ScrollRowIntoView(SharpTreeNode node, bool centre)
+ {
+ if (this.GetVisualDescendants().OfType().FirstOrDefault() is not { } scrollViewer)
return;
- }
+ if (flattener is null || flattener.IndexOf(node) is not (>= 0 and var index))
+ return;
+
+ // An expansion has just changed how many rows there are; the extent has to catch up
+ // before an offset can be clamped against it.
+ UpdateLayout();
- // If the row is already realised and roughly centred, leave the viewport alone -- this
- // keeps a re-selection of an already-centred row from twitching. We deliberately do NOT
- // skip a merely-visible row sitting at an edge: the ListBox's AutoScrollToSelectedItem
- // drags the selected row to the nearest edge first, and a reveal should still pull it to
- // the centre from there. (Skipping an already-visible row is decided one level up, before
- // AutoScroll runs, in the model->tree sync -- see TreeSelectionBinder.SyncModelToTree.)
- if (ContainerFromItem(node) is Control visible && visible.IsVisible
- && visible.TranslatePoint(new Point(0, 0), scrollViewer) is { } top)
+ double rowHeight = RowHeight();
+ double viewport = scrollViewer.Viewport.Height;
+ double rowTop = index * rowHeight;
+ double offset = scrollViewer.Offset.Y;
+
+ if (centre)
{
- var rowMid = top.Y + visible.Bounds.Height / 2;
- var viewportMid = scrollViewer.Viewport.Height / 2;
- if (Math.Abs(rowMid - viewportMid) <= visible.Bounds.Height)
+ double rowMidInViewport = rowTop - offset + rowHeight / 2;
+ if (Math.Abs(rowMidInViewport - viewport / 2) <= rowHeight)
return;
+ offset = rowTop - (viewport - rowHeight) / 2;
}
-
- // Bring it on screen (edge), force layout so the container realises, then offset so the
- // row sits at the vertical centre.
- ScrollIntoView(node);
- UpdateLayout();
- if (ContainerFromItem(node) is not Control row)
- return;
- if (row.TranslatePoint(new Point(0, 0), scrollViewer) is not { } rowTop)
+ else if (rowTop < offset)
+ {
+ offset = rowTop;
+ }
+ else if (rowTop + rowHeight > offset + viewport)
+ {
+ offset = rowTop + rowHeight - viewport;
+ }
+ else
+ {
return;
- var desiredTop = (scrollViewer.Viewport.Height - row.Bounds.Height) / 2;
- var newOffsetY = scrollViewer.Offset.Y + (rowTop.Y - desiredTop);
- var maxOffset = Math.Max(0, scrollViewer.Extent.Height - scrollViewer.Viewport.Height);
- newOffsetY = Math.Clamp(newOffsetY, 0, maxOffset);
- scrollViewer.Offset = new Vector(scrollViewer.Offset.X, newOffsetY);
+ }
+
+ double maxOffset = Math.Max(0, scrollViewer.Extent.Height - viewport);
+ scrollViewer.Offset = new Vector(scrollViewer.Offset.X, Math.Clamp(offset, 0, maxOffset));
+ }
+
+ /// The height of a row, taken from one that exists rather than assumed. The
+ /// fallback matches the item template, for the moment before the first row is
+ /// realised.
+ double RowHeight()
+ {
+ foreach (var container in GetRealizedContainers())
+ {
+ if (container.Bounds.Height > 0)
+ return container.Bounds.Height;
+ }
+ return 20;
}
///