Colour the window's frame, inset Hosts like its neighbours, drop Pins
ci / android head (pull_request) Failing after 12s
ci / build and test (pull_request) Failing after 12s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Skipped

Three things one pass over the shell's chrome turned up, none of them related
to the others beyond having been looked at together.

◆ A PALE STRIP ACROSS THE TOP OF THE WINDOW ON WINDOWS, and it is not this
application's titlebar. Avalonia's Win32 backend gives a BorderOnly window
WS_BORDER | WS_THICKFRAME and then calls DwmExtendFrameIntoClientArea with
one-pixel margins on all four sides — read out of WindowImpl.UpdateWindowProperties
in 12.1.1 rather than guessed at. So DWM owns a hairline of every edge and fills
it with the system's caption and border colours, which follow the user's
personalisation settings: with "show accent colour on title bars and window
borders" on, that is blue against a near-black shell. Nothing in the visual tree
painted those pixels, which is why nothing in the visual tree could cover them.

NativeWindowFrame sets DWMWA_BORDER_COLOR and DWMWA_CAPTION_COLOR to the
window's own Background, so the hairline still exists — the resize grip is on
it, the drop shadow hangs off it — and cannot be seen. Deliberately not
DWMWA_COLOR_NONE, which removes the border outright and leaves a near-black
window with no edge at all on a dark desktop. Windows 10 gets the dark-mode
attribute and nothing else, because the two colour attributes are Windows 11
and DwmSetWindowAttribute simply answers E_INVALIDARG there.

Called from OnOpened, not the constructor: there is no platform handle until
the window is shown, and calling early is a silent no-op — which looks exactly
like a fix that does not work.

Verified on screen on Windows 11.

◆ THE HOSTS HEADER SAT A STEP LEFT OF AND ABOVE EVERY OTHER SCREEN'S. Keychain,
Snips, Logs and Pins all frame their content with Margin="26"; Hosts was on 16
a side and 20 on top. It is 26 all round now, stated per row rather than once on
the root, because the board's ScrollViewer is deliberately full-bleed so that
its scrollbar rides the pane's edge, and because a root margin would also inset
the drawer, which draws its own.

That cost the cards ten pixels, and the layout suite is what said so:
TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen failed, because
Border.tile's 224 was derived from the board's old 16-pixel margins and the grid
quietly collapses to one column at exactly the size this application guarantees.
224 becomes 214, with the arithmetic in App.axaml rewritten — it had also gone
stale in a way that hid itself, still citing the 1016 minimum and 190 rail from
before v5b, whose two changes happened to cancel.

◆ PINS LEAVES THE RAIL, and only the rail. KnownHostsScreen is still built and
still one click away, from "Host keys" on the Keys screen's own header, which
was always the second way in. The row was kept through v5b on the grounds that
the mock has no screen for approved host keys — a reason for the screen to
exist, and never a reason for a rail entry once the keychain had a door to the
same place. Two rows landing on one screen is a rail that has to be read twice.
MainWindowViewModel.IsKnownHostsShowing stays: it names a real shell state and
ShellFlowTests still asserts on it.

design-import-gaps.md recorded that row as a deliberate deviation and
manual-checks.md Phase 1.1 walked the rail entry by entry; both are corrected,
and the manual check now reaches the screen the way a user would.

The layout suite's rail row count moves from six to five with it.

153 layout tests and 446 shell tests pass. The frame is confirmed by eye; the
Hosts inset and the rail are covered by the layout suite but were not seen
running, because the instance launched to check them came up locked.
This commit is contained in:
2026-08-12 10:43:37 +02:00
parent 53ff15ba86
commit cec73010d3
8 changed files with 212 additions and 42 deletions
+16 -4
View File
@@ -98,6 +98,18 @@
<Grid ColumnDefinitions="*,Auto">
<!--
── 26 DOWN EACH SIDE, the same inset Keychain, Snips, Logs and Pins all take. ──────────────────────
Those four say it once, as Margin="26" on their own root; this screen repeats it on each of the four
rows below, and it has to. The board's ScrollViewer is the last row and is deliberately full-bleed, so
that its scrollbar rides the pane's own edge rather than floating 26 pixels inside it — a root margin
would inset the bar with everything else. It would also inset the drawer in the second column, which
draws its own edge and wants none.
It was 16 and 20 until this pass, which put the Hosts header a visible step left of and above every
other screen's. Four numbers rather than one is the cost of the two exceptions above; changing one of
them means changing all four.
-->
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,Auto,*">
<!--
@@ -107,7 +119,7 @@
buttons over a board of forty is a pair whose subject the user has to work out. The group's own
Edit/Move/Delete sit on its own heading's menu for the same reason.
-->
<Grid Grid.Row="0" Margin="16,20,16,16" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
<Grid Grid.Row="0" Margin="26,26,26,16" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
<TextBlock Grid.Column="0" Text="Hosts" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
@@ -219,7 +231,7 @@
Ctrl+K is named on it because the palette is the other way to reach a host by typing, and somebody
who has found this box should know about the one that also connects on Enter.
-->
<Border Grid.Row="1" Margin="16,0,16,16">
<Border Grid.Row="1" Margin="26,0,26,16">
<TextBox x:Name="HostFilter" Text="{Binding HostFilter}" Height="40" CornerRadius="10"
FontFamily="{StaticResource MonoFont}"
PlaceholderText="Find a host by name, address or note… · Ctrl+K searches and connects" />
@@ -232,7 +244,7 @@
one of them sits here, above the board, rather than laid over it: a card over the cards would hide
the very ticks or the very group it is asking about.
-->
<StackPanel Grid.Row="2" Margin="16,0,16,12" Spacing="10">
<StackPanel Grid.Row="2" Margin="26,0,26,12" Spacing="10">
<!--
The conflict log. The merge is only allowed to pick a winner because the value it overrode is kept
@@ -434,7 +446,7 @@
HostsScreen.axaml.cs.
-->
<ScrollViewer Grid.Row="3" x:Name="Scroll" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,20" Spacing="16">
<StackPanel Margin="26,0,26,26" Spacing="16">
<!--
Named because it is where keyboard focus lands when the terminal gives it back, and because