using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Headless.XUnit;
using Avalonia.LogicalTree;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Xunit;
using Xunit.Abstractions;
namespace IFSRenderer.AvaloniaShell.Tests;
/// <summary>
/// Measures the real panes before asserting anything about them.
/// </summary>
/// <remarks>
/// The scrollbar defect has already survived one fix that shipped with a confident but wrong
/// explanation (that <c>AllowAutoHide="False"</c> reserves layout space). So this dumps what
/// the live visual tree actually does — bar width, whether the content presenter spans the
/// bar's column, and where the scrolled content's right edge falls — rather than reasoning
/// about the template from memory.
/// </remarks>
public class ScrollBarCharacterization
{
private readonly ITestOutputHelper _out;
public ScrollBarCharacterization(ITestOutputHelper output) => _out = output;
/// <summary>
/// What a vertical scrollbar measures with NO style override, i.e. Material's own metric.
/// This is the number the fix has to beat, and it was never measured before.
/// </summary>
[AvaloniaFact]
public void Measure_the_unstyled_material_scrollbar()
{
var content = new StackPanel();
for (int i = 0; i < 60; i++)
content.Children.Add(new TextBlock { Text = $"row {i}", Height = 24 });
var viewer = new ScrollViewer
{
VerticalScrollBarVisibility = ScrollBarVisibility.Visible,
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
AllowAutoHide = false,
Content = content,
};
// No Window.Styles: this is the Material theme's own scrollbar.
var window = new Window { Width = 300, Height = 400, Content = viewer };
window.Show();
Dispatcher.UIThread.RunJobs();
window.Measure(new Size(300, 400));
window.Arrange(new Rect(0, 0, 300, 400));
Dispatcher.UIThread.RunJobs();
ScrollBar bar = viewer.GetVisualDescendants().OfType<ScrollBar>()
.FirstOrDefault(b => b.Orientation == Avalonia.Layout.Orientation.Vertical);
var presenter = viewer.GetVisualDescendants().OfType<ScrollContentPresenter>().FirstOrDefault();
_out.WriteLine($"UNSTYLED bar : {bar?.Bounds} width={bar?.Bounds.Width}");
_out.WriteLine($"UNSTYLED viewer : {viewer.Bounds}");
_out.WriteLine($"UNSTYLED presenter: {presenter?.Bounds}");
_out.WriteLine($"presenter spans the bar? " +
$"{(presenter is not null && bar is not null && presenter.Bounds.Right > bar.Bounds.Left ? "YES (overlay)" : "no (own column)")}");
}
[AvaloniaFact]
public void Measure_the_real_panes()
{
(MainWindow window, Fakes.FakeRenderQueue queue) = MainWindowHarness.Start();
window.Measure(new Size(1500, 900));
window.Arrange(new Rect(0, 0, 1500, 900));
Dispatcher.UIThread.RunJobs();
foreach (string paneName in new[] { "LeftPanel", "PropertiesPanel", "GraphPropsPanel" })
{
var pane = window.FindControl<Border>(paneName);
if (pane is null)
{
_out.WriteLine($"{paneName}: NOT FOUND");
continue;
}
ScrollViewer viewer = pane.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (viewer is null)
{
_out.WriteLine($"{paneName}: no ScrollViewer");
continue;
}
ScrollBar bar = viewer.GetVisualDescendants().OfType<ScrollBar>()
.FirstOrDefault(b => b.Orientation == Avalonia.Layout.Orientation.Vertical);
ScrollContentPresenter presenter = viewer.GetVisualDescendants()
.OfType<ScrollContentPresenter>().FirstOrDefault();
Control content = viewer.Content as Control;
_out.WriteLine($"=== {paneName} ===");
_out.WriteLine($" pane : {pane.Bounds}");
_out.WriteLine($" viewer : {viewer.Bounds}");
_out.WriteLine($" presenter : {presenter?.Bounds.ToString() ?? "(none)"}");
_out.WriteLine($" scrollbar : {bar?.Bounds.ToString() ?? "(none)"} " +
$"visible={bar?.IsVisible} width={bar?.Bounds.Width}");
_out.WriteLine($" content : {content?.Bounds.ToString() ?? "(none)"} " +
$"margin={(content as StackPanel)?.Margin}");
if (bar is not null && content is not null)
{
// Both are measured in their own coordinate spaces; translate to the viewer.
Point barInViewer = bar.TranslatePoint(new Point(0, 0), viewer) ?? default;
Point contentInViewer = content.TranslatePoint(new Point(0, 0), viewer) ?? default;
double contentRight = contentInViewer.X + content.Bounds.Width;
_out.WriteLine($" -> bar left in viewer : {barInViewer.X}");
_out.WriteLine($" -> content right in viewer: {contentRight}");
_out.WriteLine($" -> OVERLAP: {(contentRight > barInViewer.X ? "YES" : "no")}" +
$" (by {contentRight - barInViewer.X}px)");
}
var thumb = bar?.GetVisualDescendants().OfType<Thumb>().FirstOrDefault();
_out.WriteLine($" thumb : {thumb?.Bounds.ToString() ?? "(none)"}");
}
// NB: never Close() a window in these tests. MainWindow's Closing handler cancels the
// close and awaits the exit-confirmation dialog, which nothing answers headlessly —
// the test deadlocks rather than failing.
}
}