Merge branch 'main'
ci / build and test (push) Successful in 2m3s
ci / android head (push) Successful in 3m21s
ci / desktop nightly (push) Successful in 45s
ci / api image (push) Successful in 33s

Two of main's changes land in files this branch rewrote, and both needed carrying
across by hand rather than by the merge.

The phone's nav staying up on Connections with nothing running is a fourth input
to RefreshChrome, which this branch had already given two more — whether hosts are
ticked and whether the host editor is filling the screen. They compose: the rail
and the bottom bar now ask (pages || connectPage) && !editing, so a page-shaped
terminal surface keeps its way off the screen and the editor still takes the whole
display.

The key question under the host's move panel is the harder one, because this
branch deleted the panel it was added to. The connect card is gone and the phone's
only route to a move is the action bar, so leaving the merge to take this side
would have removed a capability main had just shipped — silently, since nothing
would fail to build. It is asked in the action bar's own picker instead, in two
shapes fewer than the desktop's: one host, because which key to carry is a fact
about one machine and a selection of six has six answers, and a move rather than a
copy, because taking the key out from under an original that is staying put would
leave that original unable to connect. BindingOfTheMovingHost splits into
MovableBindingOf so both heads answer it the same way from different panels.

Main also fixed a real trap in the same commit — a host that only inherited its
key from its group arrived in the destination naming nothing at all, because the
group stays behind — and the batch move had the same bug for the same reason. It
goes through Detached now, which is where that fix lives.

The carried host is written as the carry left it rather than being detached again,
which is the one thing worth measuring: the key takes a new id over there, so a
run that rebuilt the payload from the row would send the machine across naming a
tombstone. Both directions are pinned, along with the rule about which shapes the
question is asked in at all.
This commit is contained in:
2026-08-06 09:30:00 +02:00
20 changed files with 2530 additions and 113 deletions
@@ -146,10 +146,19 @@ internal sealed partial class PhoneShell : UserControl
AvaloniaProperty.Register<PhoneShell, bool>(nameof(IsWide));
/// <summary>Whether the rail down the left edge is drawn.</summary>
/// <remarks>
/// The wide surface's answer to <see cref="ShowsBottomBarProperty"/> and asks the same question about
/// which screen is up, so the two move together — including over Connections with nothing running. See
/// <see cref="RefreshChrome"/>.
/// </remarks>
public static readonly StyledProperty<bool> ShowsRailProperty =
AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsRail));
/// <summary>Whether the three-entry bar across the bottom is drawn.</summary>
/// <remarks>
/// Not simply the pages: it also stays up on Connections with nothing running, which is the terminal
/// surface drawing a page. See <see cref="RefreshChrome"/>.
/// </remarks>
public static readonly StyledProperty<bool> ShowsBottomBarProperty =
AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsBottomBar));
@@ -212,12 +221,21 @@ internal sealed partial class PhoneShell : UserControl
/// </summary>
/// <remarks>
/// <para>
/// ◆ <b>Three flags computed here rather than three conditions in the markup, because Avalonia's
/// ◆ <b>Five flags computed here rather than five conditions in the markup, because Avalonia's
/// bindings have no "and" and none of these is a single question any more.</b> Everywhere else on this
/// head that costs a wrapper element; here it would cost two nested ones per row and the header's would
/// have to be an "or", which a wrapper cannot express at all.
/// </para>
/// <para>
/// ◆ <b>The nav stands down for a shell rather than for the terminal surface, and those parted company
/// when that surface gained a page.</b> Connections with nothing running is a box, a CONNECT button and
/// the machines connected to before — see TerminalScreen.axaml — and none of that is worth the screen a
/// shell is worth it for. It is also the one screen somebody can arrive at by closing their last tab,
/// which made the collapsed bar a way to end up with no route to Hosts or Settings but the system back
/// gesture. The header is deliberately not part of this: the surface draws its own bar with the back
/// arrow and the +, and the vault header above that is the second row this head exists to avoid.
/// </para>
/// <para>
/// <b>The header is the one worth reading twice.</b> Narrow, it stands down behind SETTINGS, because the
/// screens under that hub draw their own header with a back arrow and two rows of chrome is what this
/// surface exists to avoid. Wide, there is no hub to be behind and no back arrow to duplicate — the rail
@@ -245,6 +263,14 @@ internal sealed partial class PhoneShell : UserControl
var wide = body.Bounds.Width >= WideAt;
var pages = shell?.IsShowingPages == true;
// ◆ Connections with nothing running, which is the terminal surface drawing a page: a box, a CONNECT
// button and the machines connected to before. The nav stands down for a shell — the whole of the
// arrangement below — and there is no shell here to stand down for, so it stays. Taking it away on
// this one screen was worst where it was least affordable: somebody who has just closed their last
// tab, or who pressed Connections to see what was open and found nothing, was left on a screen whose
// only way to Hosts or Settings was the system back gesture.
var connectPage = shell is { IsTerminalSurface: true, HasTabs: false };
// The editor is a page of its own now, so nothing else is drawn around it — not the vault header,
// not the shells strip, and not the way off the screen. Its own header carries the back arrow, which
// is the one control it needs and the one the system gesture already maps to.
@@ -266,8 +292,8 @@ internal sealed partial class PhoneShell : UserControl
}
IsWide = wide;
ShowsRail = wide && pages && !editing;
ShowsBottomBar = !wide && pages && !editing;
ShowsRail = wide && (pages || connectPage) && !editing;
ShowsBottomBar = !wide && (pages || connectPage) && !editing;
ShowsShellStrip = pages && !editing;
ShowsHostSelectionBar = pages && choosing && !editing;
ShowsVaultHeader = pages && !editing && !choosing && (wide || shell?.IsMoreSurface != true);