Let the launcher icon catch up with the mark v2 already drew everywhere else

TitleBar and PhoneShell have drawn >_ knocked out of a solid accent tile since
v2, and both say in a comment that the launcher icon draws the same thing so the
three cannot drift. The launcher was still v1: an outlined square in the accent.
The comments are true now.

On Android the tile is the background layer rather than a rounded rectangle in
the foreground, because the rounding a launcher applies is its mask. Drawing the
tile would put a second rounded shape inside the mask, clipped at the corners on
any device whose mask is not the one it was drawn for; leaving it to the mask
gets a squircle on one device and a circle on another for free. Windows has no
mask, so dodossh.ico still draws its own corners.

The monochrome layer keeps only the glyph. A themed icon drops the background,
which is where the tile now lives, and a layer filled to the edges to stand in
for it would tint to a featureless square.

The glyph is half the width of the safe zone, taken from the two headers rather
than invented -- both set >_ a little under half the tile across. It is also
markedly more legible small than the outline was: dark on accent at 16px is a
mark, where a sub-pixel hairline was a smudge.
This commit is contained in:
2026-08-03 14:45:18 +02:00
parent c7c844f7bd
commit d6cd8825fc
6 changed files with 71 additions and 68 deletions
@@ -1,25 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The launcher mark, and it is the same mark ServerScreen and LockedScreen draw: a square
outline in the accent with >_ inside it. Redrawn as a vector rather than exported as a
bitmap so there is one geometry to change and no set of five PNG densities to forget one
of.
The launcher mark, and it is the same mark PhoneShell's header and the desktop titlebar draw:
>_ in the canvas colour on a solid accent tile. Filled rather than outlined since v2.
#5B8CFF is AccentColor from DodoSSH.Client.Shell's Theme/Palette.axaml, written out
because an Android resource cannot reference a XAML dictionary. The same duplication
colors.xml already carries for the window background, and the same rule applies: if the
palette moves, this moves with it.
The tile is not in this file. It is the background layer — @color/dodo_accent, see ic_launcher.xml
— and that is the whole trick of the filled design on Android: the rounding a launcher applies is
its mask, so letting the mask make the tile gets a squircle on one device and a circle on another
without either being drawn here. A rounded rectangle painted into this layer would be a second
rounded shape inside the first, visibly clipped at the corners on any device whose mask is not the
one it was drawn for.
108x108 with the artwork inside the middle 72 is the adaptive-icon contract — the outer
18 on each edge is what the launcher eats for masking and parallax. That 72 is a width,
though, and the mark is a square: its corners are what a circular mask reaches first. At
48 across the corners land 33.9 out against a radius of 36 and read as clipped even
though they technically clear it. 42 puts them at 29.7, which is margin one can see.
#0E1220 is AccentInk from DodoSSH.Client.Shell's Theme/Palette.axaml, written out because an
Android resource cannot reference a XAML dictionary. It equals Canvas today and is named
separately in the palette for a reason worth keeping in mind here: this is ink on the accent, not
the window behind it, and it follows AccentInk if the two ever part.
The stroke widths are the one place this deliberately departs from the screen. In the app
the box is a 1px border on 44px; scaled honestly that would be 1.0 here, and a launcher
drawing this at 48dp would render it at half a pixel and show nothing. 2.2 and 2.8 are
what keep it reading as the same hairline mark at the size it is actually looked at.
108x108 with the artwork inside the middle 72 is the adaptive-icon contract — the outer 18 on each
edge is what the launcher eats for masking and parallax. The glyph spans 36.06..71.94, which is
half the width of that 72 and centred in it. That half is taken from the headers rather than
invented: the phone draws >_ at font size 10 on a 26px tile and the titlebar at 9 on 20px, both a
little under half the tile across, and a launcher icon is looked at from further away than either.
The stroke width is the one place this deliberately departs from the screen. In the app the glyph
is a bold mono face whose stems come out near a fifth of its height; 5.4 here is nearer a quarter,
which is what keeps it reading as the same mark at the size it is actually looked at.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
@@ -27,28 +31,21 @@
android:viewportWidth="108"
android:viewportHeight="108">
<!-- The box: 42 across, centred, square-cornered as the Border in the app is. -->
<path
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
android:fillColor="#00000000"
android:strokeColor="#5B8CFF"
android:strokeWidth="2.2" />
<!-- The chevron of >_ -->
<path
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
android:pathData="M36.06,43.33 L49.91,53.57 L36.06,63.8"
android:fillColor="#00000000"
android:strokeColor="#5B8CFF"
android:strokeWidth="2.8"
android:strokeColor="#0E1220"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- The underscore, on the baseline the chevron bottoms out at. -->
<path
android:pathData="M54,59.7 L63.2,59.7"
android:pathData="M54,64.68 L71.94,64.68"
android:fillColor="#00000000"
android:strokeColor="#5B8CFF"
android:strokeWidth="2.8"
android:strokeColor="#0E1220"
android:strokeWidth="5.4"
android:strokeLineCap="round" />
</vector>
@@ -4,9 +4,14 @@
the wallpaper's colours. The system tints this by its alpha and discards the colour, so
the geometry is the foreground's and white is only a way of saying "opaque here".
Note what that means for the filled design: the accent tile is the background layer, and a
themed icon drops the background entirely. So the shape that survives here is the glyph, not
the tile — which is the right way round anyway. Filling this layer to the edges to stand in
for the tile would tint to a featureless square with nothing of the mark left in it.
Worth shipping rather than leaving out: a launcher with themed icons on and no monochrome
layer to use falls back to the full-colour icon, so the one app on the home screen still
drawn in green is this one.
drawn in blue is this one.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
@@ -15,24 +20,18 @@
android:viewportHeight="108">
<path
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
android:pathData="M36.06,43.33 L49.91,53.57 L36.06,63.8"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.2" />
<path
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.8"
android:strokeWidth="5.4"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M54,59.7 L63.2,59.7"
android:pathData="M54,64.68 L71.94,64.68"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.8"
android:strokeWidth="5.4"
android:strokeLineCap="round" />
</vector>
@@ -5,12 +5,17 @@
adaptive icons landed in 26, so there is no device this ships to that would need the
bitmaps. Density buckets exist to pick a PNG; a vector has nothing to pick between.
The background is the same @color/dodo_window the window, status bar and navigation bar
use, so the mark sits on the app's own near-black rather than on a second dark that is
almost but not quite it.
The background is the accent, and that is the tile itself rather than a backdrop for one:
the v2 mark is >_ knocked out of a solid accent square, so the square is this layer and the
launcher's mask is what rounds it. See ic_launcher_foreground.xml for why the rounding is
left to the mask instead of drawn.
It was @color/dodo_window until the mark went from outlined to filled, which is worth
knowing if a home screen still shows the dark version: a launcher caches icons, and the
cache outlives the install that changed them.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/dodo_window" />
<background android:drawable="@color/dodo_accent" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -10,4 +10,11 @@
hidden.
-->
<color name="dodo_window">#0E1220</color>
<!--
AccentColor from the same palette, here because the launcher icon's background layer is a colour
and not a drawable. Same hand-kept duplication as above, and the same rule: if the palette moves,
this moves with it.
-->
<color name="dodo_accent">#5B8CFF</color>
</resources>
+18 -23
View File
@@ -11,15 +11,19 @@
# Coordinates are the launcher's 108-unit viewport, mapped so the middle 72 fills the canvas.
# That 72 is not an arbitrary crop: it is the part of an adaptive icon a launcher actually shows,
# the outer 18 on each edge being what it eats for masking and parallax. Rendering the whole 108
# here would draw the mark at 39% of the width — correct arithmetic, and a stamp lost in a field
# of background. Matching what the phone displays means matching the 72.
# here would draw the glyph at half the size it is meant to be — correct arithmetic, and a stamp
# lost in a field of accent. Matching what the phone displays means matching the 72.
#
# The one thing this draws that the phone's vector does not is the tile. On Android the tile is
# the background layer and the launcher's mask rounds it; Windows has no mask, so the rounding
# happens here.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$window = [System.Drawing.ColorTranslator]::FromHtml('#0E1220') # dodo_window / CanvasColor
$accent = [System.Drawing.ColorTranslator]::FromHtml('#5B8CFF') # AccentColor
$accent = [System.Drawing.ColorTranslator]::FromHtml('#5B8CFF') # dodo_accent / AccentColor
$ink = [System.Drawing.ColorTranslator]::FromHtml('#0E1220') # AccentInk
# Every size Windows asks for: 16 in a titlebar and a tree, 32 on the desktop, 48 in a large-icon
# view, 256 for the preview pane. Shipping fewer means Windows downsamples one of these to fill
@@ -33,7 +37,7 @@ function New-MarkPng([int]$size)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
# The background, rounded as a launcher mask rounds it. A square-cornered tile would be the
# The accent tile, rounded as a launcher mask rounds it. A square-cornered tile would be the
# one icon on the taskbar with corners, which reads as unfinished rather than as deliberate.
$radius = [double]$size * 0.22
$d = $radius * 2.0
@@ -43,7 +47,7 @@ function New-MarkPng([int]$size)
$path.AddArc($size - $d, $size - $d, $d, $d, 0, 90)
$path.AddArc(0.0, $size - $d, $d, $d, 90, 90)
$path.CloseFigure()
$brush = New-Object System.Drawing.SolidBrush($window)
$brush = New-Object System.Drawing.SolidBrush($accent)
$g.FillPath($brush, $path)
# 108-viewport units to pixels, with the outer 18 dropped on each edge.
@@ -51,30 +55,21 @@ function New-MarkPng([int]$size)
function P([double]$x, [double]$y) { New-Object System.Drawing.PointF((($x - 18.0) * $scale), (($y - 18.0) * $scale)) }
# A stroke thinner than a pixel renders as a grey suggestion of itself, which at 16px is the
# difference between a mark and a smudge. The phone's file already bumps these widths for the
# difference between a mark and a smudge. The phone's file already bumps this width for the
# same reason at 48dp; the floor is that argument carried down to the sizes Windows asks for.
function Pen([double]$units)
{
$pen = New-Object System.Drawing.Pen($accent, [float][Math]::Max($units * $scale, 1.0))
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
$pen.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
return $pen
}
# The box: 42 across, centred, square-cornered as the Border in the app is.
$box = Pen 2.2
$g.DrawPolygon($box, @((P 33 33), (P 75 33), (P 75 75), (P 33 75)))
$pen = New-Object System.Drawing.Pen($ink, [float][Math]::Max(5.4 * $scale, 1.0))
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
$pen.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
# The chevron of >_ and the underscore on the baseline it bottoms out at.
$ink = Pen 2.8
$g.DrawLines($ink, @((P 44.8 48.75), (P 51.9 54), (P 44.8 59.25)))
$g.DrawLine($ink, (P 54 59.7), (P 63.2 59.7))
$g.DrawLines($pen, @((P 36.06 43.33), (P 49.91 53.57), (P 36.06 63.8)))
$g.DrawLine($pen, (P 54 64.68), (P 71.94 64.68))
$stream = New-Object System.IO.MemoryStream
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
$box.Dispose(); $ink.Dispose(); $brush.Dispose(); $path.Dispose(); $g.Dispose(); $bitmap.Dispose()
$pen.Dispose(); $brush.Dispose(); $path.Dispose(); $g.Dispose(); $bitmap.Dispose()
return $stream.ToArray()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB