Give the rest of the button shapes their content alignment too
ci / build and test (push) Successful in 2m28s
ci / desktop nightly (push) Successful in 50s
ci / api image (push) Successful in 24s
ci / android head (push) Successful in 3m15s

The sweep the ghost/accent/danger fix implied: navuser, poprow, panechip,
chiptoggle and choice each set VerticalContentAlignment now, because each set
everything else about how its content sits and left that one to Avalonia's
Stretch default.

None of them was misbehaving. Every one is content-sized everywhere it is used
today, so Stretch and Center agreed and this moves nothing — 113 buttons across
29 screens and cards measured byte-identical before and after, the strips that
have no height of their own included. What it buys is that the day one of them
is given a height, it is already right rather than quietly drawing its label in
the top third.

flat and cat are deliberately NOT swept in, and the reasoning that would sweep
them is exactly the trap. flat carries the titlebar's search pill, a
Border.searchpill with no height of its own that is meant to fill all 35 pixels
of its button — the usage states HorizontalContentAlignment="Stretch" and takes
the vertical default to match. cat carries the keychain rail's accent strip, a
Border.rowmark whose style sets Width="2" and no height at all, "at full row
height" by its own remark. Centring either from the style shrinks a pill and a
strip that are correct today. AStretchingShapeStillFillsItsButton pins both, and
fails when flat is centred.

ButtonCaptionTests covers the five new shapes on the existing rule. Its
stretch-fill assertion reads the content slot off the presenter rather than
recomputing it from the button's Padding: the shapes differ in whether their
presenter also draws a border, and a hand-rolled sum was two pixels out on
Button.cat for that reason.
This commit is contained in:
2026-08-10 10:20:26 +02:00
parent 9bc9069425
commit 0ffd259ccd
2 changed files with 87 additions and 1 deletions
+5
View File
@@ -153,6 +153,7 @@
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="6,2" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontWeight" Value="Medium" />
@@ -463,6 +464,7 @@
<Setter Property="Padding" Value="13,7" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="CornerRadius" Value="12" />
</Style>
<Style Selector="Button.navuser /template/ ContentPresenter#PART_ContentPresenter">
@@ -482,6 +484,7 @@
<Style Selector="Button.poprow">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Padding" Value="11,4" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="MinHeight" Value="20" />
@@ -671,6 +674,7 @@
-->
<Style Selector="Button.choice">
<Setter Property="Padding" Value="10,5" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10.5" />
<Setter Property="LetterSpacing" Value="0.5" />
@@ -1120,6 +1124,7 @@
<Style Selector="Button.panechip">
<Setter Property="Padding" Value="8,4" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="FontWeight" Value="Medium" />
@@ -1,13 +1,14 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
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.
/// The button shapes that centre their caption do, and the two that deliberately do not still fill.
/// </summary>
/// <remarks>
/// <para>
@@ -41,6 +42,15 @@ namespace DodoSSH.Client.App.Layout.Tests;
/// 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>
/// <para>
/// The five shapes beyond the original three were swept in afterwards, and none of them was misbehaving
/// when it was: every one is content-sized everywhere it is used today, so <c>Stretch</c> and <c>Center</c>
/// agreed and the change moved nothing — 113 buttons across 29 screens measured byte-identical before and
/// after. What the sweep buys is that the day any of them is given a height, it is already right. That is
/// also why <see cref="AStretchingShapeStillFillsItsButton"/> matters more than it looks: the same
/// reasoning applied to <c>flat</c> or <c>cat</c> would break a pill and a strip that are currently
/// correct.
/// </para>
/// </remarks>
public sealed class ButtonCaptionTests
{
@@ -61,6 +71,11 @@ public sealed class ButtonCaptionTests
[InlineData("ghost")]
[InlineData("accent")]
[InlineData("danger")]
[InlineData("navuser")]
[InlineData("poprow")]
[InlineData("panechip")]
[InlineData("chiptoggle")]
[InlineData("choice")]
public async Task ACaptionIsCentredInAButtonTallerThanItself(string shape)
{
await MeasureAsync(
@@ -96,6 +111,72 @@ public sealed class ButtonCaptionTests
"the caption should sit high, which is the defect this suite was written for"));
}
/// <summary>
/// <c>flat</c> and <c>cat</c> are excluded from the rule above, and must stay excluded.
/// </summary>
/// <remarks>
/// <para>
/// Both stretch their content on purpose, and both would be silently broken by a later pass that
/// "finished" the sweep the rest of these classes belong to — which is exactly why this is a test and
/// not a comment.
/// </para>
/// <para>
/// <c>flat</c> carries the titlebar's search pill, a <c>Border.searchpill</c> with no height of its own
/// that is meant to fill all 35 pixels of the button; the usage states
/// <c>HorizontalContentAlignment="Stretch"</c> and relies on the vertical default matching it. Centring
/// from the style would shrink that pill to its caption's line box inside a button twice as tall.
/// <c>cat</c> carries the keychain rail's accent strip, a <c>Border.rowmark</c> whose style sets
/// <c>Width="2"</c> and no height at all — "at full row height", says the rule's own remark — so its
/// height is the stretch and nothing else.
/// </para>
/// <para>
/// Asserted as "the content fills the button", not as "the caption is off-centre": what these two need
/// is the fill, and a test phrased the other way would still pass if the fill broke in some new way.
/// </para>
/// </remarks>
[Theory]
[InlineData("flat")]
[InlineData("cat")]
public async Task AStretchingShapeStillFillsItsButton(string shape)
{
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
// A bare Border is what both of them actually hold: no height, sized only by its parent.
var fill = new Border();
var button = new Button { Content = fill, Height = FixedHeight };
button.Classes.Add(shape);
var window = LayoutHarness.HostAtMinimumSize(
button, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight);
try
{
// The slot read off the presenter rather than recomputed from the button's Padding:
// these shapes differ in whether their presenter also draws a border, and a hand-rolled
// sum was two pixels out on Button.cat for exactly that reason.
var presenter = button.GetVisualDescendants()
.OfType<ContentPresenter>()
.Single(p => string.Equals(p.Name, "PART_ContentPresenter", StringComparison.Ordinal));
var slot = presenter.Bounds.Height
- presenter.Padding.Top - presenter.Padding.Bottom
- presenter.BorderThickness.Top - presenter.BorderThickness.Bottom;
fill.Bounds.Height.ShouldBe(
slot,
Tolerance,
$"Button.{shape} must stretch its content — the search pill and the rail's accent "
+ "strip have no height of their own");
}
finally
{
window.Close();
}
},
Token);
}
private static Task MeasureAsync(
string shape, VerticalAlignment? alignment, Action<double, double> assert) =>
LayoutHarness.OnTheUiThreadAsync(