From c7c844f7bd36d1ae8805110892bb5bc6eba428a8 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Mon, 3 Aug 2026 14:34:39 +0200 Subject: [PATCH] Give the desktop head the mark the phone has carried all along The window had no icon and the executable had no icon, so Windows drew its generic one in the taskbar, in alt-tab and in Explorer, next to an application that draws its own titlebar precisely so that nothing about it looks borrowed. The geometry is the launcher vector's, mapped so the middle 72 of its 108-unit viewport fills the canvas: that 72 is what an adaptive icon actually shows, and rendering the full 108 would draw the mark at 39% of the width. Nine sizes, because Windows asks for several and downsamples unkindly when one is missing, and a floor of one pixel on the strokes so 16px is a mark rather than a smudge. Windows takes an .ico and nothing else, so the raster the phone does not need exists here. dodossh-icon.ps1 sits beside it and draws it from those same numbers, so the two heads still have one geometry between them. --- .../Assets/dodossh-icon.ps1 | 124 ++++++++++++++++++ src/DodoSSH.Client.App/Assets/dodossh.ico | Bin 0 -> 9397 bytes .../DodoSSH.Client.App.csproj | 16 +++ src/DodoSSH.Client.App/Views/MainWindow.axaml | 1 + 4 files changed, 141 insertions(+) create mode 100644 src/DodoSSH.Client.App/Assets/dodossh-icon.ps1 create mode 100644 src/DodoSSH.Client.App/Assets/dodossh.ico diff --git a/src/DodoSSH.Client.App/Assets/dodossh-icon.ps1 b/src/DodoSSH.Client.App/Assets/dodossh-icon.ps1 new file mode 100644 index 0000000..0093416 --- /dev/null +++ b/src/DodoSSH.Client.App/Assets/dodossh-icon.ps1 @@ -0,0 +1,124 @@ +# Regenerates dodossh.ico from the same geometry the Android launcher icon draws. +# +# The phone's mark is a vector — Resources/drawable/ic_launcher_foreground.xml — and the whole +# reason it is a vector is that there is then one geometry to change and no set of PNG densities +# to forget one of. Windows will not take a vector: wants an .ico and nothing +# else, and Window.Icon wants a bitmap. So the raster exists, and this script is how it stays +# honest: the numbers below are the ones in that XML, and regenerating is the whole edit. +# +# pwsh -File src/DodoSSH.Client.App/Assets/dodossh-icon.ps1 +# +# 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. + +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 + +# 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 +# the gap, and its downsampler is not kind to a hairline. +$sizes = @(16, 20, 24, 32, 40, 48, 64, 128, 256) + +function New-MarkPng([int]$size) +{ + $bitmap = New-Object System.Drawing.Bitmap($size, $size, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) + $g = [System.Drawing.Graphics]::FromImage($bitmap) + $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 + # one icon on the taskbar with corners, which reads as unfinished rather than as deliberate. + $radius = [double]$size * 0.22 + $d = $radius * 2.0 + $path = New-Object System.Drawing.Drawing2D.GraphicsPath + $path.AddArc(0.0, 0.0, $d, $d, 180, 90) + $path.AddArc($size - $d, 0.0, $d, $d, 270, 90) + $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) + $g.FillPath($brush, $path) + + # 108-viewport units to pixels, with the outer 18 dropped on each edge. + $scale = [double]$size / 72.0 + 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 + # 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))) + + # 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)) + + $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() + return $stream.ToArray() +} + +# ICO is a six-byte header, a sixteen-byte directory entry per image, then the images. The entries +# carry PNG payloads rather than the older BMP-with-AND-mask form, which every Windows since Vista +# reads and which is what keeps a 256px entry from costing 256KB. +# +# The cast on each frame is load-bearing. A byte[] returned through PowerShell's output collector +# comes back as Object[] of boxed bytes, which BinaryWriter has no overload for — it binds to one +# of the scalar Write()s instead and puts a single byte on the stream. The first run of this +# script produced a 159-byte .ico that way, header and directory intact and nine one-byte images. +$frames = New-Object 'System.Collections.Generic.List[byte[]]' +foreach ($size in $sizes) +{ + [byte[]]$png = New-MarkPng $size + $frames.Add($png) +} + +$out = New-Object System.IO.MemoryStream +$w = New-Object System.IO.BinaryWriter($out) +$w.Write([uint16]0) # reserved +$w.Write([uint16]1) # type: icon +$w.Write([uint16]$sizes.Count) + +$offset = 6 + (16 * $sizes.Count) +for ($i = 0; $i -lt $sizes.Count; $i++) +{ + $size = $sizes[$i] + $w.Write([byte]($(if ($size -ge 256) { 0 } else { $size }))) # 0 means 256 + $w.Write([byte]($(if ($size -ge 256) { 0 } else { $size }))) + $w.Write([byte]0) # palette entries: none, this is truecolour + $w.Write([byte]0) # reserved + $w.Write([uint16]1) # colour planes + $w.Write([uint16]32) # bits per pixel + $w.Write([uint32]$frames[$i].Length) + $w.Write([uint32]$offset) + $offset += $frames[$i].Length +} + +foreach ($frame in $frames) { $w.Write($frame) } +$w.Flush() + +$target = Join-Path $PSScriptRoot 'dodossh.ico' +[System.IO.File]::WriteAllBytes($target, $out.ToArray()) +$w.Dispose(); $out.Dispose() + +Write-Output "Wrote $target ($($sizes.Count) sizes, $((Get-Item $target).Length) bytes)" diff --git a/src/DodoSSH.Client.App/Assets/dodossh.ico b/src/DodoSSH.Client.App/Assets/dodossh.ico new file mode 100644 index 0000000000000000000000000000000000000000..c3326f9fd31fb97e9044bc6a0d12e1d1dcaf501a GIT binary patch literal 9397 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 e{C0app.manifest true + + Assets/dodossh.ico + + + + diff --git a/src/DodoSSH.Client.App/Views/MainWindow.axaml b/src/DodoSSH.Client.App/Views/MainWindow.axaml index 4888f41..f2d1f9b 100644 --- a/src/DodoSSH.Client.App/Views/MainWindow.axaml +++ b/src/DodoSSH.Client.App/Views/MainWindow.axaml @@ -6,6 +6,7 @@ x:Class="DodoSSH.Client.App.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Title="DodoSSH" + Icon="/Assets/dodossh.ico" Width="1180" Height="760" MinWidth="1016"