Public Access
App.axaml's Button.ghost, Button.accent, Button.danger rule set VerticalAlignment and never VerticalContentAlignment. The first places the button in its parent; the second places the caption in the button, and its default is Stretch — so on any of these given a fixed Height the content presenter stretched the caption TextBlock to the whole content box, and a TextBlock draws its line at the top of whatever it is given. Measured on the hosts toolbar, whose three buttons are 40 pixels: nine above the ink and twenty below it. Every box was the height it declared, which is why this read as one of them being the wrong height — nothing was mis-sized, the labels sat in the top third. Center rather than a hand-tuned Padding, because the gap is the difference between the line box and the content box and moves with the font size: these carry 11.5 by default and the primary action overrides it to 13.5. It is the three shapes that were missed rather than a new idiom — navseg, sesstab, headerghost, sidebarrow, fieldrow and paneicon all state it already, as does every one of the phone head's own button classes. 134 buttons carry these three classes; the ones that show it are those with an explicit Height, which is both toolbars, the drawer's Save/Cancel pair, and the import screen. A button sized to its own caption was already right and is untouched. HorizontalContentAlignment is deliberately left alone: it is Stretch too and invisible on a self-sized button, and the flyout rows that are stretched wide ask for Left themselves. ButtonCaptionTests measures a bare Button, since Application.Styles is global and a screen-level test would pin one toolbar and leave the rest to the same defect. It measures the laid-out line rather than the TextBlock's arranged bounds, and that distinction is the test: under Stretch those bounds fill the content box and so are symmetrical whether or not the ink in them is. The first draft asserted on them and passed against the defect; the calibration test caught it, and against the old markup all three shapes now fail naming their own gap.
139 lines
6.6 KiB
C#
139 lines
6.6 KiB
C#
using System.Globalization;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Layout;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace DodoSSH.Client.App.Layout.Tests;
|
|
|
|
/// <summary>
|
|
/// The three button shapes centre their caption inside a button taller than the caption.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// ◆ THE DEFECT THIS EXISTS FOR was read as a height problem and was not one. <c>App.axaml</c>'s
|
|
/// <c>Button.ghost, Button.accent, Button.danger</c> rule set <c>VerticalAlignment</c> — where the button
|
|
/// sits in its parent — and never <c>VerticalContentAlignment</c>, where the caption sits in the button.
|
|
/// Avalonia's default for the second is <c>Stretch</c>, so on any of these given a fixed <c>Height</c> the
|
|
/// content presenter stretched the caption's <see cref="TextBlock"/> to the whole content box, and a
|
|
/// <see cref="TextBlock"/> draws its line at the TOP of its bounds. The hosts toolbar's three 40-pixel
|
|
/// buttons measured 9 pixels above the ink and 20 below it. Every box was the height it declared, which is
|
|
/// exactly why it read as one being wrong: nothing was mis-sized, the labels sat in the top third.
|
|
/// </para>
|
|
/// <para>
|
|
/// A bare <see cref="Button"/> rather than a screen, because <c>Application.Styles</c> is global — the same
|
|
/// property <see cref="LayoutHarnessTests"/> leans on — so this measures the style rule itself rather than
|
|
/// one of the hundred-odd places it lands. A screen-level test would pin one toolbar and leave the dialogs,
|
|
/// the drawer's Save/Cancel pair and the import screen's buttons uncovered by the thing that fixed them all.
|
|
/// </para>
|
|
/// <para>
|
|
/// ◆ THE LINE BOX IS MEASURED, NOT THE TextBlock's ARRANGED BOUNDS, and the difference is the whole test.
|
|
/// A stretched caption's <see cref="Visual.Bounds"/> fill the content box, so they are symmetrical about
|
|
/// the button's middle under the defect just as they are under the fix — the first draft of this suite
|
|
/// asserted on them, passed on both, and was caught only by
|
|
/// <see cref="AStretchedCaption_IsNotCentred_AndIsCaught"/>. What actually moves is where the line sits
|
|
/// INSIDE those bounds: <see cref="TextBlock"/> draws at the top of whatever it is given, so the ink is
|
|
/// centred only when the box it is drawn in is its own line box.
|
|
/// </para>
|
|
/// <para>
|
|
/// The gaps are compared to each other rather than to a number. What the caption's own line box measures is
|
|
/// a property of JetBrains Mono at whichever size the caller set — 11.5 by default and 13.5 on the primary
|
|
/// action — so an absolute expectation would be a font metric written down in a test file, and it would move
|
|
/// the day the face does. "Centred" survives both.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class ButtonCaptionTests
|
|
{
|
|
/// <summary>Taller than any caption these carry, which is the condition that exposes the defect.</summary>
|
|
/// <remarks>
|
|
/// The hosts and keychain toolbars' own number. A button left to size itself cannot show this at all:
|
|
/// its content box is its caption's line box exactly, so <c>Stretch</c> and <c>Center</c> agree and a
|
|
/// test written against one would pass under either.
|
|
/// </remarks>
|
|
private const double FixedHeight = 40;
|
|
|
|
/// <remarks>One pixel, for a content box whose odd leftover cannot be halved evenly.</remarks>
|
|
private const double Tolerance = 1;
|
|
|
|
private static CancellationToken Token => TestContext.Current.CancellationToken;
|
|
|
|
[Theory]
|
|
[InlineData("ghost")]
|
|
[InlineData("accent")]
|
|
[InlineData("danger")]
|
|
public async Task ACaptionIsCentredInAButtonTallerThanItself(string shape)
|
|
{
|
|
await MeasureAsync(
|
|
shape,
|
|
alignment: null,
|
|
(above, below) =>
|
|
Math.Abs(above - below).ShouldBeLessThanOrEqualTo(
|
|
Tolerance,
|
|
string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"Button.{shape} left {above:0.#} above the caption and {below:0.#} below it.")));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The instrument's own calibration: the check fails on the arrangement the fix replaced.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The same practice <see cref="LayoutHarnessTests"/> states for the clipping harness, and it earns its
|
|
/// place here for the same reason. Everything above passes if <c>VerticalContentAlignment</c> is Center
|
|
/// AND it passes if the caption happens to fill its box, so without this a rule quietly reverted to
|
|
/// Stretch would have to be caught by eye again. Stretch is set inline here rather than by editing the
|
|
/// style sheet, because <c>Application.Styles</c> is global and a test that mutated it would be changing
|
|
/// every other test in the assembly out from under itself.
|
|
/// </remarks>
|
|
[Fact]
|
|
public async Task AStretchedCaption_IsNotCentred_AndIsCaught()
|
|
{
|
|
await MeasureAsync(
|
|
"ghost",
|
|
VerticalAlignment.Stretch,
|
|
(above, below) => (below - above).ShouldBeGreaterThan(
|
|
Tolerance,
|
|
"the caption should sit high, which is the defect this suite was written for"));
|
|
}
|
|
|
|
private static Task MeasureAsync(
|
|
string shape, VerticalAlignment? alignment, Action<double, double> assert) =>
|
|
LayoutHarness.OnTheUiThreadAsync(
|
|
() =>
|
|
{
|
|
var button = new Button { Content = "Group ▾", Height = FixedHeight };
|
|
button.Classes.Add(shape);
|
|
|
|
if (alignment is { } forced)
|
|
{
|
|
button.VerticalContentAlignment = forced;
|
|
}
|
|
|
|
var window = LayoutHarness.HostAtMinimumSize(
|
|
button, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight);
|
|
|
|
try
|
|
{
|
|
var caption = button.GetVisualDescendants().OfType<TextBlock>().Single();
|
|
var origin = caption.TranslatePoint(default, button);
|
|
|
|
origin.ShouldNotBeNull("the caption is not in the button's visual tree");
|
|
|
|
// The laid-out line rather than caption.Bounds.Height, which under Stretch is the
|
|
// content box and so is symmetrical whether or not the ink in it is — see the remark on
|
|
// the class.
|
|
var line = caption.TextLayout.Height;
|
|
|
|
var above = origin.Value.Y;
|
|
var below = button.Bounds.Height - above - line;
|
|
|
|
assert(above, below);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
},
|
|
Token);
|
|
}
|