OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / ScrollBarCharacterization.cs

134 lines · csharp ref:
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.
    }
}