Files
DodoSSH/src/DodoSSH.Client.Android/Theme/Phone.axaml
T
jaap-jan 8a77b7ca68
ci / build and test (pull_request) Failing after 2m34s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Skipped
ci / android head (pull_request) Successful in 3m28s
Say how far a connection has got while it is still being made
The connecting card set its status string once, when the tab was created, and
never touched it again. Every connection therefore looked identical from the
outside: one three seconds into a key exchange, one waiting out a fifteen-second
timeout against a machine that is asleep, and one that had hung all drew the
same "connecting…". The card now draws the five steps of getting there, each lit
at the moment the handshake reports reaching it, over an amber track that fills
as they finish.

◆ NOTHING ON THE LIST IS INVENTED. Every row changes state because a layer below
it said so, at the instant the thing it names actually began.

That is the whole reason it is worth showing, and it is why most of this commit
is plumbing rather than XAML: there was no progress reporting anywhere in the
stack to hook a step list onto, and a card animating plausible progress would
have been indistinguishable from one that had stopped receiving any.

SshConnectionPhase names four phases and deliberately not more. SSH.NET runs the
entire handshake inside one ConnectAsync and raises exactly one event from the
middle of it — HostKeyReceived, once the key exchange has produced a key to show
— so that event is the only interior moment there is to report. Everything
before it is Reaching and everything after it is Authenticating. A fifth phase
in that assembly would have to be a timer, so there is not one. OpeningShell is
reported by TerminalWorkspace instead, because that is where it happens: the
factory's work ends with an authenticated connection, and asking for a
pseudo-terminal on one is a separate round trip. The SFTP path passes null — a
second connection opened behind an already-open shell has nobody watching a step
list for it.

The card's fifth step, "Starting the terminal", is the renderer wait and lives
in the shell rather than in the SSH assembly, which has never heard of a
renderer. On the first connection after a cold start it is a real wait with a
real failure mode of its own — a missing WebView2 runtime — so a list that began
at "reaching the host" would leave the one wait most likely to hang unnamed.

Amber for the step in flight, and that follows the palette's rule rather than
bending it. Green is what is true and purple is what you can press; a step still
happening is neither, and it is exactly the caveat-worth-reading that amber
exists for. Steps behind it go green as they become true. Nothing animates,
which is the argument TransfersScreen.axaml already makes for its own track,
reaching a screen with far more reason to want a spinner: a spinner is furniture
invented to fill a state nobody measured, and these states are measured, so the
track fills to what has finished and then waits there.

A refusal keeps the step it stopped on, in red, with the ones behind it still
green. That is the half a progress bar could not do, and it is the difference
between "that host is not there" and "that host is there and would not have me"
— a question the reason sentence alone frequently does not settle.

The strip's dot goes amber while a tab is connecting, on both heads. It was
grey, and so is a tab whose shell has exited: the two states in that strip with
the least in common, one worth waiting for and one over. PhoneShell's own
comment already recorded half of this — the dot stopped being green before
anything had answered — and this is the other half.

Progress is raised inline rather than through System.Progress<T>, which captures
whatever synchronisation context it was constructed on and posts to it. That
reads like a convenience and is really a second place the marshalling decision
gets made: silently, differently under a test with no context, and out of order
with respect to the failure that follows a phase. The shell marshals once, in
one handler, through a new optional post parameter on MainWindowViewModel — the
same seam TransfersViewModel already uses, and for the reason its own remark
gives. The three Dispatcher.UIThread.Post calls that predate it are the ones
this suite's comments record as out of reach; they are left alone rather than
swept in here.

Both heads draw the list. They differ in one place: Phone.axaml's mono class
sets a colour and a size along with the family, so the caption rule names its
own family instead of composing the two and asking two rules for one Foreground.
The desktop's mono sets the family alone, which is why ConnectingCard does
compose them. Each head also gains SHOW LOGS beside the button that gives up —
the step list is this attempt and the log is every other one, which is what a
connection taking too long actually raises.

Seven tests, and the two that matter most run against the container rather than
a fake: a real handshake reports its phases in order, and a host-key refusal
never claims to have authenticated. A fake asserting what it was written to
assert would have established nothing about either. The rest cover the tab
advancing while the connection is gated, the step a refusal stops on, and a
phase reported after the user has given up on the tab. 1,861 tests, none
failing.

The Android head's layout is not verified by anything. It compiles, and
compiled bindings mean every new binding path resolves, but that project is not
in DodoSSH.slnx, there is no test project for it and no device here — so unlike
the desktop card, whose shapes the layout harness measures, these rows have not
been drawn. Vertical fit is reasoned, not observed.
2026-08-10 15:47:45 +02:00

543 lines
30 KiB
XML

<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
The phone's own control styles. The desktop head has an equivalent block in its App.axaml and the two
deliberately do not share one: the palette is shared because a colour must not drift, but a control's
size is a property of the input device. Every touch target here is at least 44 effective pixels tall,
which is the number the desktop's 28-pixel rows would fail.
Classes rather than implicit styles for the buttons, because the shell needs three that differ in
meaning rather than in degree: the accent one is what the screen wants you to do, the outlined one is
the alternative, and the red one is the one that costs something.
── v2 ──────────────────────────────────────────────────────────────────────────────────────────────
The second design rounded everything on its own ladder — 4 for a tag, 9 for a button or a pill, 10 for
a list row, 11 for the search well, 12 for a card, 14 for a block of monospaced output — and the numbers
below carried it for three passes: the radius said how big the thing was, so a 12 on a chip or a 4 on a
card read as the wrong size before it read as the wrong shape. Left as a record of that reasoning rather
than deleted, because the reasoning still holds; only the numbers it was reasoning about have moved.
── v5 ──────────────────────────────────────────────────────────────────────────────────────────────
This pass takes the desktop's v5 ladder instead of v2's own, on the same reversal recorded for the
fonts: the phone now matches the desktop's shape as well as its face. Three rungs rather than six —
cards and sections stay 12, a button or a field is 10, a chip or a tag is 6 — collapsing v2's 9/10/11
into the one value the desktop's buttons and fields already use, and moving its 4 up to 6 and its 14
down to 12 to land on the desktop's own chip and card numbers. The ladder still says how big a thing
is before it says what shape it is; it is just a shorter ladder now, because the desktop it is copying
never drew v2's 11-radius search well or a card any rounder than a chip's neighbour a step away, and the
same case that closed the gap between 9, 10 and 11 closes the one between 12 and 14.
Not every radius on this head belongs to this ladder. The floating action button is 28 — half its own
56, which is a circle rather than a ladder rung — and the sheets stay at 22 on their top corners only,
which is a phone idiom this codebase's own bottom sheets have used since v2 and the desktop draws
nothing like. Both are documented where they are set rather than here, for the reason FAB and sheet
radii are always documented locally: a reader who only ever meets one of them should not have to find
this paragraph to learn it was deliberate.
-->
<Style Selector="Button.primary">
<Setter Property="Height" Value="48" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
◆ Gradient and glow, replacing a flat Accent fill — the same swap App.axaml's Button.accent made on the
desktop, and for the same reason: this design draws every primary button top-to-bottom from a lighter
violet into the accent proper, lifted off the surface with a soft violet glow rather than a border. Both
live in Palette.axaml as AccentGradient and AccentGlow, already resolvable here because the palette is
shared — this is a resource-key swap, not new colour.
Background and BoxShadow are set here rather than as plain Setters above, because Fluent's default
button template only lets a style reach the fill and the glow through the ContentPresenter it draws
itself around — the same reason the desktop's rule targets /template/ ContentPresenter rather than the
Button. Height, radius and the rest stay ordinary Setters on Button.primary itself; only the two the
template intercepts move down here.
-->
<Style Selector="Button.primary /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="BoxShadow" Value="{StaticResource AccentGlow}" />
</Style>
<Style Selector="Button.primary:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="Opacity" Value="0.82" />
</Style>
<!--
Disabled is drawn as flat and unlit rather than merely dimmed. The design's CONTINUE button on the
recovery screen is disabled until the checkbox is ticked, and a user who cannot tell it is disabled
reads the screen as broken rather than as waiting for them.
BoxShadow has to be cleared here too, as "none" rather than left unset — see the remark on
Button.accent:disabled in the desktop's App.axaml for why the literal string is required and an empty
BoxShadows is not: the base rule's glow Setter is still in effect wherever a more specific one does not
override it, and a disabled primary button lit with a glow would read as wanting to be pressed.
-->
<Style Selector="Button.primary:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Raised}" />
<Setter Property="BoxShadow" Value="none" />
<Setter Property="TextElement.Foreground" Value="{StaticResource TextFaint}" />
</Style>
<Style Selector="Button.secondary">
<Setter Property="Height" Value="48" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="11.5" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Height" Value="48" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource DangerSoft}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource Danger}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10.5" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
A row in a list: the whole row is the target, and it is 54 tall because a thumb is not a mouse.
◆ VerticalContentAlignment, because that height is the whole point of this class and Avalonia's default
for content alignment is Stretch — so the content presenter stretched the caption to the full row and a
TextBlock draws its line at the TOP of what it is given. Most rows here never showed it, having a
StackPanel or a Grid of already-centred children in them, which is what made the four that did look
like four unrelated mistakes: the breadcrumb chips and the up-one-directory button on FilesScreen, and
TerminalScreen's CLOSE THIS TAB, each a bare TextBlock in a row 36 or 44 tall with no vertical padding.
Measured at those numbers, the caption sat flush against the top edge with 21 to 33 pixels below it.
The desktop head's App.axaml carries the same setter on its own shapes for the same reason, and excludes
two of them — see the remark on Button.ghost there. Nothing is excluded here: no row's content depends
on being stretched, there being no full-height strip inside any of the thirty-three, and the Grids that
stop filling hold only children that already centre themselves, so they land where they always did.
-->
<Style Selector="Button.row">
<Setter Property="MinHeight" Value="54" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="16,6" />
</Style>
<Style Selector="Button.row:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Hover}" />
</Style>
<!--
The 44-pixel square in a header — a back arrow, an add, an upload. Square rather than stretched because
it sits beside a title that must keep the width it has, and 44 rather than the glyph's own size because
a 17-pixel arrow is a 17-pixel target.
-->
<Style Selector="Button.icon">
<Setter Property="Width" Value="44" />
<Setter Property="Height" Value="44" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="17" />
</Style>
<Style Selector="Button.icon.accent">
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
<Setter Property="FontSize" Value="20" />
</Style>
<Style Selector="Button.icon:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
</Style>
<!--
The floating action button the design has asked for since v2, and which this theme refused twice — on
the grounds that it would open an editor this head had not got, and that an unused style is a claim the
control exists somewhere. Both were true. The second is why the style arrives in the same commit as the
button rather than ahead of it.
56 rather than the 44 the header icons use. It is the only control on the screen that floats over
content rather than sitting in a bar, so it has no neighbouring chrome to borrow an edge from, and a
thumb reaching the bottom-right corner of a 360dp screen is doing it without looking.
Accent-filled, which it shares with Button.primary and with nothing else — and it means the same thing
in both places: the one action on the surface that is not a choice between peers. Circular by radius
rather than by a Path, so the pressed state the template draws is the same shape as the button — 28,
exactly half its own 56, which is a geometric constraint rather than a ladder rung and does not move
with the rest of this file's radii.
◆ Gradient and glow since v5, matching Button.primary rather than staying a flat fill once that one
moved. The two are the only accent-filled controls on this head and are read as one idea — "the thing
this surface wants you to do" — so a flat FAB beside a gradient primary button would be the seam this
codebase's palette file keeps warning about, one screenshot over from the button it echoes. The glow
reads as well on a floating circle as it does on a bar-anchored rectangle: if anything a control that
already floats over content earns a lift more than one sitting in a row of chrome does, so it is kept
rather than dropped for the FAB's own shape.
Still only on HOSTS. The design puts a second one on S3 and that editor genuinely does not exist yet, so
the style being here is not permission to draw one there.
-->
<Style Selector="Button.fab">
<Setter Property="Width" Value="56" />
<Setter Property="Height" Value="56" />
<Setter Property="Padding" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="28" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!-- See the remark above Button.primary's own /template/ ContentPresenter rule: BoxShadow has no home on
a Button itself, so the fill moves down here alongside it rather than staying a plain Setter. -->
<Style Selector="Button.fab /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="BoxShadow" Value="{StaticResource AccentGlow}" />
</Style>
<Style Selector="Button.fab:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="Opacity" Value="0.82" />
</Style>
<!--
The dimmed area behind a sheet, and a Button rather than a Border because tapping it has to mean
something — see the add sheet in HostsScreen, which is dismissible for the reason the host-key sheet is
not.
62% of Canvas, written out because the palette holds no alpha variant of a surface. The pressed rule is
not decoration: a Button's default template lights its ContentPresenter when touched, so without it the
scrim would flash a pale rectangle across the whole screen on the way to closing — which reads as the
interface glitching rather than as a tap being received.
-->
<Style Selector="Button.scrim">
<Setter Property="Background" Value="#9E05050A" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="Button.scrim:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#9E05050A" />
</Style>
<Style Selector="Button.scrim:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#9E05050A" />
</Style>
<!--
A card: one item, one rule, one thing you can act on. The design uses it for a key, a snippet, a
transfer and the panel at the foot of the keychain, and they are all the same shape deliberately —
the screens differ in what they hold rather than in how they are built.
-->
<Style Selector="Border.card">
<Setter Property="Background" Value="{StaticResource Sidebar}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="14,13" />
</Style>
<!--
A block of monospaced output — a snippet's command, a log, the terminal. One surface for all three, and
it is not one of the palette's chrome greys: see TerminalSurface in Palette.axaml.
-->
<Style Selector="Border.output">
<Setter Property="Background" Value="{StaticResource TerminalSurface}" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="12" />
</Style>
<!--
A small monospaced tag: a vault's name, a sync badge, what is open on the files screen. Not a control —
it is never pressed — so a Border rather than a Button, and the class exists so that the four screens
drawing one cannot disagree about its padding.
-->
<Style Selector="Border.tag">
<Setter Property="Background" Value="{StaticResource Chip}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="7,2" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Border.tag > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="9" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<!--
A tag that is also a button: the picker in the host editor. It borrows Border.tag's geometry rather than
inheriting it, because a Button is not a Border and Avalonia's selectors are structural — the numbers
are repeated and the two have to move together, which is why they sit adjacent.
Worn is filled, unworn is outlined, so a row of these reads as the host's tags rather than as a form
about them. 36 tall rather than 44: it is a chip, its neighbours are chips, and a row of 44-pixel pills
would be a wall. The whole chip is the target and they are spaced 6 apart, which is what keeps a miss
landing on nothing rather than on the wrong tag.
-->
<Style Selector="Button.chiptoggle">
<Setter Property="MinHeight" Value="36" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="11,0" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.chiptoggle.worn">
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<Style Selector="Button.chiptoggle.worn /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Chip}" />
</Style>
<Style Selector="Button.chiptoggle:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Hover}" />
</Style>
<!-- An outlined tag, for a fact about an item rather than a name: a type, an algorithm, a count. -->
<Style Selector="Border.tag.outline">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="5,1" />
</Style>
<Style Selector="Border.tag.outline > TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="8.5" />
</Style>
<!--
A category chip. A RadioButton rather than a ToggleButton because the options are exclusive and a screen
reader should say so; the bullet its default template draws is removed, since the chip's own fill is
what carries the state.
Checked is a filled surface with accent *text*, not an accent fill. v2 makes that distinction
everywhere — see the remark on AccentText in Palette.axaml — and a chip is where it matters most: a row
of four solid blue lozenges is a row of four things that all look like the primary action.
◆ Its own radius rather than Border.tag's or Button.chiptoggle's, despite the name. v2 drew this control
on the ladder's button-or-pill rung rather than its tag rung — 9, not 4 — and the v5 pass carries the
same rung forward to 10 rather than to Border.tag's 6: it is a much larger control at 34 tall against a
tag's line-height, and a radius picked for a 34-pixel chip is not the one a 20-pixel tag needs, whatever
the class is called. See the remark on Phone.axaml's own ladder, above.
-->
<Style Selector="RadioButton.chip">
<Setter Property="MinHeight" Value="34" />
<Setter Property="Padding" Value="13,6" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10.5" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="Chip" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}"
MinHeight="{TemplateBinding MinHeight}">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="RadioButton.chip:checked">
<Setter Property="Background" Value="{StaticResource Active}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
No segmented control either. The design draws one over the file listing for LOCAL / REMOTE, and there is
no local side on a phone — see FilesScreen. The two-way choices that are real, the log's two logs and
the keychain's categories, are chip rows, which is the same control at the same size without a well
around it.
-->
<Style Selector="TextBox.field">
<Setter Property="Height" Value="44" />
<Setter Property="Padding" Value="14,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource Field}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
</Style>
<!--
Every box on this head that takes a secret, and it is a class rather than two attributes repeated five
times because the two attributes are not interchangeable and both are needed.
<b>PasswordChar is what the screen shows; ContentType is what the keyboard is told.</b> The desktop head
needs only the first — a Windows text box has no opinion about what is being typed into it. Android's
software keyboard does: left at its default it treats a passphrase box as ordinary prose, which means
the suggestion strip offers completions from everything that phone has ever been typed into, and the
IME's own learning dictionary remembers what was typed. Dots on screen and a word-suggestion bar above
them is the worst of both — the secret is hidden from the person typing it and offered to the room.
TextInputOptions.ContentType is the property the Android backend maps onto InputType, and Password is
what turns the suggestions off and keeps the entry out of the dictionary.
A field class is not implied. Two of the five boxes carry their own metrics — see LockedScreen — so this
one sets nothing about size or colour, and the other three say Classes="field secret".
-->
<Style Selector="TextBox.secret">
<Setter Property="PasswordChar" Value="•" />
<Setter Property="TextInputOptions.ContentType" Value="Password" />
</Style>
<!--
The live dot, and the class name is the same one the desktop sidebar uses so the two heads cannot
drift on what green means: a terminal is open on this host right now. Deliberately not reachability —
nothing pings anything, and a dot meaning "up" would be a claim the application never checks.
Live rather than Accent since v2. The accent is blue now and this is not an accent: it is a fact about
a host. See the remark on Live in Palette.axaml.
-->
<Style Selector="Ellipse.dot">
<Setter Property="Width" Value="7" />
<Setter Property="Height" Value="7" />
<Setter Property="Fill" Value="{StaticResource TextGhost}" />
</Style>
<Style Selector="Ellipse.dot.live">
<Setter Property="Fill" Value="{StaticResource Live}" />
</Style>
<!--
Amber, and it does not contradict the remark above. That one says green is a fact about a host rather
than an accent, and this is the colour for a fact that is not settled yet: green is what is true, purple
is what you can press, and a connection still being made is neither. The palette's own rule gives amber
to the caveat worth reading, which is exactly what this is.
Only the tab strips use it, and only for a tab with no shell behind it yet — the same amber, from the
same brush, as the track and the running step on the connecting screen, so that a tab and the screen it
opens agree about what is happening. See TerminalScreen.axaml.
-->
<Style Selector="Ellipse.dot.connecting">
<Setter Property="Fill" Value="{StaticResource Warn}" />
</Style>
<!-- Every label, count, address and fingerprint in this design is monospace. See Palette.axaml. -->
<Style Selector="TextBlock.mono">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="TextBlock.label">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="9.5" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="TextBlock.detail">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
<Setter Property="FontSize" Value="10" />
</Style>
<Style Selector="TextBlock.body">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="LineHeight" Value="19" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="TextBlock.title">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
A screen's own name, in the header. The one place this design is not monospaced: the design sets every
heading in its sans face and keeps the mono for what is machine-shaped — an address, a fingerprint, a
count, a command. That split is worth honouring even though both resolve to substituted fonts here.
-->
<Style Selector="TextBlock.heading">
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="17" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!--
A group heading over a list: PRODUCTION, STAGING. Letter-spaced, and quieter than the rows under it —
but TextDim rather than the design's own #565E7E. In a flat list these headings are the only thing
separating one block of hosts from the next, and at 10px with a letter-space the fainter step is under
3:1 on this background. Quieter than a host row, still readable, which is what "quieter" has to mean.
-->
<Style Selector="TextBlock.section">
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="LetterSpacing" Value="1" />
</Style>
<!--
A fingerprint, and it is a style rather than four repeated setters because the rule it encodes is a
product rule: never truncated, and always breakable across lines rather than clipped. Four screens show
one, and the day one of them clips is the day somebody approves the wrong key.
-->
<!--
◆ Declared twice, and this is the bug the rule above exists to prevent rather than a tidiness problem.
Avalonia's type selectors are exact: `TextBlock.fingerprint` does not match a SelectableTextBlock, which
derives from it. Every fingerprint on this head is selectable — you want to be able to copy one and
compare it — so with only the first selector the style applied to none of them, and a SHA256 fingerprint
rendered in the proportional font at the inherited size with TextWrapping left at NoWrap. At 360dp that
runs off the edge and clips, which is exactly the thing the comment above says must never happen.
The desktop head already carries the same pair for `mono` (App.axaml), for the same reason. Avalonia has
an `:is(TextBlock)` form that would match both in one selector; two explicit lines are used here because
the duplication is the thing worth seeing when somebody adds a third fingerprint site.
-->
<Style Selector="TextBlock.fingerprint">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="LineHeight" Value="18" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="SelectableTextBlock.fingerprint">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="LineHeight" Value="18" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
</Styles>