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 0000000..c3326f9 Binary files /dev/null and b/src/DodoSSH.Client.App/Assets/dodossh.ico differ diff --git a/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj b/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj index c52905d..38f9352 100644 --- a/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj +++ b/src/DodoSSH.Client.App/DodoSSH.Client.App.csproj @@ -5,6 +5,14 @@ app.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"