using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Xunit;
namespace IFSRenderer.AvaloniaShell.Tests;
/// <summary>
/// "this scroll bar is weird" and, after a first attempted fix, "they still overlap other
/// windows, and shouldn't be so fat".
/// </summary>
/// <remarks>
/// <para>
/// The measured mechanism, which the first fix got wrong: Avalonia's ScrollViewer template
/// lets the ScrollContentPresenter span the scrollbar's column, so the bar is an OVERLAY.
/// <c>AllowAutoHide="False"</c> — the first fix — only stops it collapsing to a hairline; it
/// reserves no space whatsoever. Material's own bar measures 16px, the panes left a 12px
/// gutter, so content ran 4px underneath it.
/// </para>
/// <para>
/// The fix is therefore two numbers that have to agree: the bar is pinned narrow, and every
/// scrolled pane leaves a gutter wider than it. These tests measure the real window, because
/// the previous attempt was reasoned rather than measured and shipped broken.
/// </para>
/// </remarks>
public class ScrollBarTests
{
/// <summary>Widest a vertical bar may be before it reads as "fat". Material's default is 16.</summary>
private const double MaxBarWidth = 12;
private static readonly string[] ScrolledPanes = ["LeftPanel", "PropertiesPanel"];
private static (MainWindow Window, Fakes.FakeRenderQueue Queue) LaidOutWindow()
{
(MainWindow window, Fakes.FakeRenderQueue queue) = MainWindowHarness.Start();
window.Measure(new Size(1500, 900));
window.Arrange(new Rect(0, 0, 1500, 900));
Dispatcher.UIThread.RunJobs();
return (window, queue);
}
private static (ScrollBar Bar, Control Content) PaneParts(MainWindow window, string paneName)
{
var pane = window.FindControl<Border>(paneName);
Assert.NotNull(pane);
ScrollViewer viewer = pane.GetVisualDescendants().OfType<ScrollViewer>().First();
ScrollBar bar = viewer.GetVisualDescendants().OfType<ScrollBar>()
.FirstOrDefault(b => b.Orientation == Avalonia.Layout.Orientation.Vertical);
return (bar, viewer.Content as Control);
}
[AvaloniaFact]
public void The_scrollbar_is_not_fat()
{
(MainWindow window, _) = LaidOutWindow();
foreach (string pane in ScrolledPanes)
{
(ScrollBar bar, _) = PaneParts(window, pane);
Assert.True(bar is not null && bar.IsVisible, $"{pane}: expected a visible scrollbar");
Assert.True(bar.Bounds.Width <= MaxBarWidth,
$"{pane}: scrollbar is {bar.Bounds.Width}px wide, expected <= {MaxBarWidth}px");
}
}
/// <summary>The reported defect: the bar is drawn on top of the controls.</summary>
[AvaloniaFact]
public void Scrolled_content_never_runs_under_the_scrollbar()
{
(MainWindow window, _) = LaidOutWindow();
foreach (string paneName in ScrolledPanes)
{
(ScrollBar bar, Control content) = PaneParts(window, paneName);
Assert.NotNull(content);
Assert.True(bar is not null && bar.IsVisible, $"{paneName}: expected a visible scrollbar");
double contentRight = content.Bounds.Right;
double barLeft = bar.Bounds.Left;
Assert.True(contentRight <= barLeft,
$"{paneName}: content ends at {contentRight}px but the bar starts at {barLeft}px "
+ $"— overlapping by {contentRight - barLeft}px");
}
}
/// <summary>
/// Keeps the two numbers honest. The bar is an overlay, so the gutter is the ONLY thing
/// keeping content clear of it — if the gutter ever shrinks below the bar width again, the
/// assertion above starts failing rather than the overlap going unnoticed.
/// </summary>
[AvaloniaFact]
public void The_presenter_really_does_span_the_scrollbar()
{
(MainWindow window, _) = LaidOutWindow();
var pane = window.FindControl<Border>("LeftPanel");
ScrollViewer viewer = pane.GetVisualDescendants().OfType<ScrollViewer>().First();
var presenter = viewer.GetVisualDescendants().OfType<ScrollContentPresenter>().First();
ScrollBar bar = viewer.GetVisualDescendants().OfType<ScrollBar>()
.First(b => b.Orientation == Avalonia.Layout.Orientation.Vertical);
// If this ever stops being true, the template changed and the gutter is no longer
// needed — but until then, removing the gutter reintroduces the reported bug.
Assert.True(presenter.Bounds.Right > bar.Bounds.Left,
"the ScrollContentPresenter no longer spans the scrollbar column; re-derive the "
+ "gutter from the new template instead of assuming an overlay");
}
}