Public Access
Colour the window's frame, inset Hosts like its neighbours, drop Pins
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:
@@ -300,7 +300,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> | The status bar's negotiated cipher, host-key algorithm and key/credential name | ◆ **Shipped, on both surfaces, with three honest deviations.** `ISshConnection` and `ISftpSession` now both carry `Cipher` — the server-to-client algorithm off SSH.NET's own `ConnectionInfo.CurrentServerEncryption`, captured once at construction because a rekey is not an event SSH.NET raises — and `TerminalWorkspace.GetSessionFacts` hands the cipher and the host key's algorithm back to the shell the moment a session opens; `VaultViewModel.TryBuildAuthentication` now threads the authenticating key's or credential's own `Label` into `HostAuthentication.IdentityLabel`, all the way to `MainWindowViewModel`'s surface-aware `SessionCipher`, `SessionHostKeyAlgorithm` and `SessionIdentityLabel`, composed into one `SessionIdentityText` run for the status bar. Three deviations from the mock, not omissions: the algorithm prints exactly as negotiated (`ssh-ed25519`), not the design's shortened `ed25519`, because trimming it would be an edit to a string this client did not choose; the run is plain text rather than the design's clickable element, because there is no pin-details modal for a session that is already open, and drawing a click target for a screen that does not exist would itself be a fabrication; and a typed-password session — nothing filed in the keychain to name — shows the host-key algorithm alone, with no `·` after it, because there is no item behind the dot. |
|
||||
> | S3 dimmed in the design's own switcher | **Enabled.** The mock leaves S3 as future work; this application already has bucket browsing, so SSH, SFTP and S3 are a true three-way segment, wired to `IsSshShowing`, `IsTransfersShowing` and `IsBucketsShowing` exactly alike. |
|
||||
> | The S3/Buckets screen | **Did not get the session shell in v5b.** `TransfersScreen` serves both SFTP and S3 today and only the SFTP usage in `MainWindow.axaml` sat inside the new tab row/header/status bar/sidebar; the S3 usage was unchanged at the time. **v5c gives it the shell's own look without the machinery** — a 26-pixel padded, bordered, radius-12 container and nothing past that, since a bucket has no tab to close, no host to head a card with and no pin for a sidebar to show; see the v5c section, below. |
|
||||
> | No pins destination in the design at all | **Kept anyway.** The rail still carries Pins — `KnownHostsScreen` — because the mock has no screen for approved host keys and this application's has to stay reachable. |
|
||||
> | No pins destination in the design at all | **The rail agrees with the design now.** `KnownHostsScreen` is still built and still reachable — from **Host keys** on the Keys screen's own header, which was always the second way in — but the rail's Pins row is gone. It was kept through v5b on the grounds that the mock has no screen for approved host keys, which is a reason for the screen to exist and was never a reason for a rail entry once the keychain had a door to the same place. Two rail rows landing on one screen is a rail that has to be read twice. |
|
||||
> | The popover's Settings and Preferences rows, and the design's own Settings-* family of screens | **Landed in v5c.** What was two doors to one room in v5b — Settings and Preferences both opening the same bare `Preferences` screen — is now two of three doors onto their own settings pages: Settings opens General, Preferences opens Preferences, and a third row, Vaults, opens Vaults. All three are real, distinct pages inside one settings mode; see the v5c section, below. |
|
||||
> | `· Org` after the user chip's name, and a `Primary` tag on a vault row in the popover | Neither. There is no organisation concept behind a vault — only the vault itself — and no vault is distinguished as primary; the popover's vault rows are the existing shown-vaults toggles, restyled. |
|
||||
> | The design's titlebar, which has nowhere for a sync indicator | `SYNCED` stays, on the titlebar's right side, ahead of the window's own minimise/maximise/close buttons — the one thing this titlebar keeps that the design's own does not draw at all. |
|
||||
|
||||
@@ -28,8 +28,9 @@ a phase had nothing left for a person to do, which is the good outcome rather th
|
||||
|
||||
### 1.1 No screen is sliced at the WebView's left edge · **the important one**
|
||||
|
||||
Open two terminals, then visit every nav rail entry in turn — Hosts, Keys, Pins, Snips, Logs — and both of
|
||||
the switcher's other two segments, SFTP and S3, at the rail's own head.
|
||||
Open two terminals, then visit every nav rail entry in turn — Hosts, Keys, Snips, Logs — and both of
|
||||
the switcher's other two segments, SFTP and S3, at the rail's own head. The pins screen is no longer a rail
|
||||
entry; reach it from **Host keys** on the Keys screen's header and check it the same way.
|
||||
|
||||
**Pass:** each screen draws whole, its buttons all clickable, and the nav rail stays up the left edge for
|
||||
every one of them. Since v5b's chrome pass the rail is permanent furniture — it no longer collapses for
|
||||
|
||||
@@ -913,18 +913,30 @@
|
||||
into a grid: equal columns, and a card that grew a third line of tags is taller than its neighbours
|
||||
rather than narrower.
|
||||
|
||||
◆ 224 IS DERIVED, and the arithmetic is written out because getting it wrong is invisible. The grid's
|
||||
column at the window's minimum is 1016 less the rail's 190 and the drawer's 320, which is 506. The
|
||||
scrolling stack inside it takes 16 of margin on each side, and the vertical scrollbar takes its own —
|
||||
call the usable width 474. A WrapPanel fits floor(474 / (Width + 10)) per row, so two columns needs
|
||||
Width no more than 227.
|
||||
◆ 214 IS DERIVED, and the arithmetic is written out because getting it wrong is invisible. The grid's
|
||||
column at the window's minimum is 1081 less the rail's 255 and the drawer's 320, which is 506. The
|
||||
scrolling stack inside it takes 26 of margin on each side, and the vertical scrollbar takes its own —
|
||||
call the usable width 454. A WrapPanel fits floor(454 / (Width + 10)) per row, so two columns needs
|
||||
Width no more than 217, and 214 is that with the same few pixels of slack the previous number kept.
|
||||
|
||||
The first number here was 248, from the same reasoning with the two margins left out. It laid out
|
||||
cleanly and the layout harness passed it, because the harness asks whether a control is inside the
|
||||
window and not how many of them fit on a line — so the grid quietly became one column wide at exactly
|
||||
the size this application guarantees, which is the shape the cards exist to avoid. The second was 232,
|
||||
derived the same way against the drawer's own 304; v5 widened the drawer to 320 for the ADDRESS field's
|
||||
breathing room, which narrowed the budget this number is drawn from and had to move it down in step.
|
||||
Every number here has moved at least once, and always because something beside the cards did:
|
||||
|
||||
· 248, from this reasoning with the two margins left out. It laid out cleanly and the layout harness
|
||||
passed it, because the harness asks whether a control is inside the window and not how many of them
|
||||
fit on a line — so the grid quietly became one column wide at exactly the size this application
|
||||
guarantees, which is the shape the cards exist to avoid.
|
||||
· 232, derived against the drawer's own 304, which v5 widened to 320 for the ADDRESS field's breathing
|
||||
room — narrowing the budget this number is drawn from and moving it down in step.
|
||||
· 224, which is what that gave. The stated arithmetic still said 1016 and 190 by then: v5b's rail took
|
||||
190 to 255 and the window's minimum 1016 to 1081 in the same pass, so the two changes cancelled and
|
||||
the answer stayed right while the working went stale.
|
||||
· 214, now that HostsScreen's board is inset 26 a side rather than 16 — see that file's own remark on
|
||||
why every screen frames its content the same way. Twenty pixels of board is twenty pixels the cards
|
||||
no longer have, and this is where they come from.
|
||||
|
||||
◆ THE TEST THAT CATCHES THIS IS NOT THE HARNESS. See
|
||||
ScreenLayoutTests.TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen, which counts columns
|
||||
because that is the thing this number exists to buy and the thing no fit assertion can see.
|
||||
-->
|
||||
<Style Selector="Border.tile">
|
||||
<Setter Property="Background" Value="{StaticResource Raised}" />
|
||||
@@ -932,7 +944,7 @@
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Padding" Value="12,10" />
|
||||
<Setter Property="Width" Value="224" />
|
||||
<Setter Property="Width" Value="214" />
|
||||
<Setter Property="Margin" Value="0,0,10,10" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover Border.tile">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,6 +54,21 @@ internal sealed partial class MainWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Colours the system-drawn frame the moment there is a handle to colour it on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>OnOpened</c> and not the constructor: the window has no platform handle until it is shown, and
|
||||
/// <see cref="NativeWindowFrame"/> does nothing without one. See that class for what the frame is and
|
||||
/// why <c>BorderOnly</c> still has one.
|
||||
/// </remarks>
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
|
||||
NativeWindowFrame.MatchTo(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks the Linux backend for the one mode it can actually draw inside this window.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Paints the frame Windows still draws around a <c>BorderOnly</c> window in the application's own
|
||||
/// colour, so the top edge stops reading as a leftover system titlebar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>The symptom this exists for:</b> a pale strip across the very top of the window, a few pixels
|
||||
/// tall and plainly not part of the application — most obvious on a machine with "show accent colour
|
||||
/// on title bars and window borders" turned on, where it comes out blue against a near-black shell.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It is not <c>TitleBar.axaml</c> leaking and it is not a margin. It is DWM, and the reason it is
|
||||
/// there is visible in Avalonia's own Win32 backend: <c>WindowImpl.UpdateWindowProperties</c> gives a
|
||||
/// <see cref="WindowDecorations.BorderOnly"/> window <c>WS_BORDER | WS_THICKFRAME</c> and then calls
|
||||
/// <c>DwmExtendFrameIntoClientArea</c> with one-pixel margins on all four sides. So the compositor
|
||||
/// owns a hairline of every edge of this window, and it fills that hairline with the system's caption
|
||||
/// and border colours — which are chosen by the user's personalisation settings and have no reason to
|
||||
/// resemble <c>CanvasColor</c>. The window is the wrong place to look for the pixels; they were never
|
||||
/// painted by anything in this tree.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The fix is to tell DWM what colour to use rather than to try to cover it. <c>DWMWA_BORDER_COLOR</c>
|
||||
/// and <c>DWMWA_CAPTION_COLOR</c> arrived in Windows 11 21H2 and are exactly that; both are set to the
|
||||
/// window's own background, so the hairline still exists — the resize grip is on it, and the drop
|
||||
/// shadow hangs off it — and simply cannot be seen. Deliberately <em>not</em> <c>DWMWA_COLOR_NONE</c>,
|
||||
/// which removes the border outright: on a dark desktop that leaves a near-black window with no edge
|
||||
/// at all, which trades one visual defect for another.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Windows 10 gets the dark-mode attribute and nothing else, and that is the whole of what is
|
||||
/// available there: the two colour attributes are unsupported, <c>DwmSetWindowAttribute</c> answers
|
||||
/// <c>E_INVALIDARG</c>, and the calls do nothing. Hence the ignored return values — every attribute
|
||||
/// here is an improvement where it lands and a no-op where it does not, so there is nothing for a
|
||||
/// caller to handle and nothing worth logging on a path that runs once at startup.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal static class NativeWindowFrame
|
||||
{
|
||||
/// <summary>Windows 11 21H2 and later: the colour of the frame border.</summary>
|
||||
private const int BorderColorAttribute = 34;
|
||||
|
||||
/// <summary>Windows 11 21H2 and later: the colour of the caption, including the extended frame.</summary>
|
||||
private const int CaptionColorAttribute = 35;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 10 1903 and later: draw the frame in the dark palette.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Redundant on Windows 11, where the two colour attributes above name the colours outright, and it
|
||||
/// is set anyway because it is the only one of the three that Windows 10 honours. The build before
|
||||
/// 1903 used attribute 19 for this; that is not chased here, because a border on an OS release that
|
||||
/// left support in 2020 is not worth a second interop call.
|
||||
/// </remarks>
|
||||
private const int DarkModeAttribute = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Matches <paramref name="window"/>'s system-drawn frame to the colour it paints itself.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call once the window has a handle — <c>OnOpened</c> is the first such moment. Calling earlier
|
||||
/// finds no platform handle and silently does nothing, which is the defect this replaced: the strip
|
||||
/// is only visible once the window is on screen, so a call that ran too early looks like a fix that
|
||||
/// does not work rather than a fix that never ran.
|
||||
/// </remarks>
|
||||
internal static void MatchTo(Window window)
|
||||
{
|
||||
// Every attribute below is a DWM one, and DWM is Windows. Elsewhere the frame is drawn by the
|
||||
// platform's own compositor and there is nothing here to say to it.
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.TryGetPlatformHandle()?.Handle is not { } handle || handle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Set(handle, DarkModeAttribute, 1);
|
||||
|
||||
// The window's own Background rather than a named resource, so the frame cannot drift from the
|
||||
// canvas when the palette moves. A brush that is not solid — a gradient, or nothing set at all —
|
||||
// has no single colour to match, and leaving the system's own is better than inventing one.
|
||||
if (window.Background is not ISolidColorBrush { Color: var canvas })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reference = ColorRef(canvas);
|
||||
Set(handle, BorderColorAttribute, reference);
|
||||
Set(handle, CaptionColorAttribute, reference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets one integer-valued DWM attribute, and discards the answer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The discard is the point of this method existing rather than being three call sites. Every
|
||||
/// attribute here is unsupported on some Windows this application runs on, and unsupported means
|
||||
/// <c>E_INVALIDARG</c> and no change — which is the intended outcome on that OS, not a failure, so
|
||||
/// there is nothing for the caller to do with the <c>HRESULT</c> and nothing worth logging once at
|
||||
/// startup. Written once, with the reasoning, rather than left implicit at each call.
|
||||
/// </remarks>
|
||||
private static void Set(IntPtr window, int attribute, int value) =>
|
||||
_ = DwmSetWindowAttribute(window, attribute, ref value, sizeof(int));
|
||||
|
||||
/// <summary>
|
||||
/// Packs <paramref name="color"/> into a Win32 <c>COLORREF</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>0x00BBGGRR</c> — blue in the high byte, not red, and the alpha byte must be zero. Getting the
|
||||
/// order wrong produces a plausible-looking wrong colour rather than an error, which is the kind of
|
||||
/// bug that survives a glance at the window.
|
||||
/// </remarks>
|
||||
private static int ColorRef(Color color) => color.R | (color.G << 8) | (color.B << 16);
|
||||
|
||||
/// <remarks>
|
||||
/// <c>DllImport</c> rather than <c>LibraryImport</c>, for the reason
|
||||
/// <see cref="NativeKeyboardFocus"/> gives at its own P/Invoke: the generated form needs
|
||||
/// <c>AllowUnsafeBlocks</c> across a project that handles key material, and this signature is
|
||||
/// blittable, so there is no marshalling for it to improve.
|
||||
/// </remarks>
|
||||
#pragma warning disable SYSLIB1054
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmSetWindowAttribute(IntPtr window, int attribute, ref int value, int size);
|
||||
#pragma warning restore SYSLIB1054
|
||||
}
|
||||
@@ -40,8 +40,9 @@
|
||||
Both are still one click away; see the popover below the user chip. The chip itself carries the signed-
|
||||
in identity this application actually has — a display name and, where the server sent one, an email —
|
||||
which is also new: the titlebar drew an account name and a vault chip before this pass and does not any
|
||||
more. See TitleBar.axaml and design-notes/v5b-fidelity-notes.md for the deviations this rail keeps on
|
||||
purpose: Pins, which the mock has no screen for at all, and the S3 segment above.
|
||||
more. See TitleBar.axaml and design-notes/v5b-fidelity-notes.md for the one deviation this rail still
|
||||
keeps on purpose: the S3 segment above. Pins was the other, and it is gone — see the remark where that
|
||||
row used to sit, between Keys and Snips.
|
||||
|
||||
Buttons rather than a TabStrip or a ListBox, still, for the reason the v3 remark gave: all three hold
|
||||
the selection themselves, so a click would move the highlight before the shell decided anything, and a
|
||||
@@ -128,25 +129,16 @@
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
KEPT — the mock has no screen for approved host keys at all; see the file-level remark. push_pin
|
||||
is the same codepoint HostsScreen.axaml already draws for a host's own pin badge, reused rather
|
||||
than picked afresh so the one concept reads as one glyph everywhere it appears.
|
||||
◆ NO Pins ROW. The pins screen is still here and still reached in one click — from "Host keys"
|
||||
on the Keys screen's own header, which is where a list of approved host keys belongs: they are
|
||||
keychain material, and that button was already the second way to reach them. Two rail rows away
|
||||
from each other, both landing on the same screen, is a rail that has to be read twice.
|
||||
|
||||
◆ U+F10D, not U+E946, which both sites drew until this pass and which no glyph in the embedded
|
||||
face answers to: the cmap of Assets/Fonts/MaterialIcons (Material Icons 1.017, 2019) skips E944
|
||||
and E946, so this row and the hosts screen's own pin badge were both drawing a tofu box. F10D is
|
||||
where push_pin lives in that vintage, verified against the file rather than against a codepoints
|
||||
table for a later release of the font.
|
||||
It is also the last of the rail's own deviations from the mock to go. The row was kept in v5b on
|
||||
the grounds that the design has no screen for approved host keys at all — see the file-level
|
||||
remark — which is true of the design and was never a reason for a rail entry once the keychain
|
||||
had a door to the same place.
|
||||
-->
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsKnownHostsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.KnownHosts}"
|
||||
ToolTip.Tip="Host keys you have approved, and how to withdraw one">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Pins" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSnippetsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
|
||||
@@ -1596,7 +1596,7 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// v5b's redraw changes what this test has to hold. Three button shapes live in the rail now rather
|
||||
/// than one: the switcher's three segments, each a third of the rail's own content width; the six item
|
||||
/// than one: the switcher's three segments, each a third of the rail's own content width; the five item
|
||||
/// rows below it and the user chip at the foot, both the rail's full content width. A single
|
||||
/// across-the-board width assertion the way the v3 version of this test made one would either be wrong
|
||||
/// for the segments or have to loosen until it caught nothing, so each shape gets its own count and its
|
||||
@@ -1604,13 +1604,18 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The rail runs vertically, so what runs out at the window's minimum is still height — a switcher plus
|
||||
/// six rows plus a user chip have to leave room for each other in the same space the v3 rail's seven
|
||||
/// five rows plus a user chip have to leave room for each other in the same space the v3 rail's seven
|
||||
/// plain rows did. Both counts are asserted in both directions for the reason the old test's was: an
|
||||
/// entry silently dropping off the bottom would still pass every other assertion here.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Five and not six since Pins left the rail: the pins screen is reached from "Host keys" on the Keys
|
||||
/// screen, which was always the other way in. Exact rather than a bound, so putting a row back is a
|
||||
/// decision somebody makes here rather than something that slips in.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheNavRailHoldsItsSwitcherSixDestinationsAndTheUserChipAtTheWindowsMinimum()
|
||||
public async Task TheNavRailHoldsItsSwitcherFiveDestinationsAndTheUserChipAtTheWindowsMinimum()
|
||||
{
|
||||
await LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
@@ -1630,7 +1635,7 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
|
||||
segments.Count.ShouldBe(3, "SSH, SFTP and S3");
|
||||
rows.Count.ShouldBe(
|
||||
6, "the mode-dependent first row, then Hosts, Keys, Pins, Snips and Logs");
|
||||
5, "the mode-dependent first row, then Hosts, Keys, Snips and Logs");
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user