feat(clients): one Lucide icon set, and the quick-action editors to match #463

Merged
enricobuehler merged 12 commits from feat/quick-actions-lucide-icons into main 2026-08-30 23:37:33 +00:00
12 Commits
Author SHA1 Message Date
enricobuehler 5ae29d72e8 fix(client/windows): finish the avatar's OS-mark signature change
apple / swift (pull_request) Successful in 2m11s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
android / android (pull_request) Canceled after 0s
ci / rust (pull_request) Canceled after 0s
ci / rust-arm64 (pull_request) Canceled after 0s
ci / web (pull_request) Canceled after 0s
ci / docs-site (pull_request) Canceled after 0s
ci / bun-nix (pull_request) Canceled after 0s
ci / docs-drift (pull_request) Canceled after 0s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Canceled after 2m53s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Canceled after 0s
`windows-client.yml` fails to build this branch on both arm64 and x64,
four errors, all from the commit that gave the host card an OS mark:

  hosts.rs:144,145  doc comment on a function parameter
  pair.rs:97        avatar(&target.name)  — argument #2 missing
  stream.rs:37      avatar(&host)         — argument #2 missing

`avatar` grew a second parameter and two of its three call sites were
not updated. Neither has an OS chain to pass: `Target` carries name,
address, port, fingerprint, MAC and profile, and no OS field. So both
pass `""`, which is what `avatar` documents as "keeps the monogram" —
and what `stream.rs`'s own header comment already said it wanted.

The parameter comment becomes `//`. Its text was worth keeping; rustc
simply will not take `///` there.

⚠ NOT compiled locally: this crate cannot be built for Windows from a
Mac, and scripts/xcheck.sh does not carry punktfunk-client-windows. CI
is the verifier.
2026-08-31 01:29:26 +02:00
enricobuehler 39697cdd68 fix(client/linux): hold the slot picker open with a real minimum width
apple / swift (pull_request) Successful in 2m14s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 2m1s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 2m7s
ci / web (pull_request) Successful in 3m9s
ci / docs-site (pull_request) Successful in 1m50s
ci / rust-arm64 (pull_request) Successful in 4m13s
ci / docs-drift (pull_request) Successful in 40s
ci / bun-nix (pull_request) Successful in 1m2s
ci / rust (pull_request) Failing after 10m9s
android / android (pull_request) Successful in 10m11s
The popover collapsed to a column a character or two wide. The earlier
guess — that a ScrolledWindow asks for its minimum unless told to
propagate the natural one — was only half of it: with the horizontal
policy set to Never, `min_content_width` is IGNORED and the scrolled
window propagates its child's minimum instead. The child is a ListBox
of AdwActionRows whose titles wrap, and a wrapping label's minimum
width is one word, so the whole popover shrank to that.

`set_size_request` is a minimum GTK cannot ignore. The natural width
still grows it to fit the rows, up to the existing ceiling.
2026-08-31 00:53:58 +02:00
enricobuehler a04c8cee2d feat(clients): the host card's circle carries the OS mark, not a letter
Which machine this is answers a more useful question than which letter
its name starts with, and the name is spelled out directly under the
circle anyway. The mark moves out of the status row, where it was the
smallest glyph on the card, and becomes its leading visual.

A host that advertises no OS chain — an older one — keeps the initial,
so nothing regresses to an empty circle.

GTK sets the icon on the Adwaita avatar, which needs show_initials off
first: the widget prefers initials whenever it is allowed to. The
generated background colour stays, and the symbolic mark recolors with
the theme like every other glyph there.

Windows has no theme-aware tint, so the bake changes colour with the
move: mid-grey was chosen to sit on a card face, and it read as smudged
the moment the mark landed on the accent fill. It bakes white now, and
taller, since it is a 22 DIP avatar face rather than a 16 DIP row glyph.
One bake, not two — the row was the grey one's only reader.
2026-08-31 00:51:24 +02:00
enricobuehler 6dbaafa764 fix(client/linux): centre what sits on a disc, widen the slot picker
Three layout faults in the GTK editor, two of them one mistake.

A gtk::Box is HORIZONTAL by default and packs a single child at the
start, so a child narrower than the box's requested size lands on its
left edge no matter how it aligns itself. That put the centre disc's
mark and the shortcut preview's chord against the left rim. Both fixes
are the same: let the child expand, so its own centre alignment has
space to centre within.

The slot picker opened at its 320 px floor because a ScrolledWindow
asks for its minimum width unless told to propagate the natural one —
which wrapped entries like "Disconnect, keep the game running" into a
narrow column. It now sizes to the list, between a floor and a ceiling.
2026-08-31 00:44:32 +02:00
enricobuehler 9a0c03a02e fix(console): the icon test reads the table's third column
Adding the font codepoint to the shared table widened every ALL entry
to three fields; the console's parse-and-fit test still destructured
two. Test-only, so the plain build stayed green and only clippy
--all-targets caught it.
2026-08-31 00:26:35 +02:00
enricobuehler ce5cc10082 fix(clients): the slot picker drops its second way to do one thing
Both desktop editors offered "Swap with <clock>" rows beside the
catalogue. Dragging one disc onto another already swaps them, and any
slot can be set outright from the catalogue above — so the six rows
only made the list longer.

On Windows the card also carried a title naming the slot and a Done
button. The band above the ring names the slot being edited, the open
disc is ringed white, and a pick now closes the card, which is what the
GTK popover always did. Neither had anything left to say.
2026-08-31 00:24:42 +02:00
enricobuehler dfc10b68e4 fix(client/windows): the font conversion left two dead branches
Clippy caught both, and both are the conversion's own shadow: DIM_INK
lost its last reader when the ring's mark stopped being an opacity on
an Image, and the reset button's armed/unarmed icons collapsed into the
same call once the white bake was gone. The accent fill already says
the button is armed, and the glyph takes the on-accent brush itself.
2026-08-31 00:22:20 +02:00
enricobuehler b50248168b fix(client/windows): draw the icon font, stop rasterizing the icons
Baking PNGs was the wrong call and it showed twice over. Reactor's
`.icon()` builds a BitmapIcon with `ShowAsMonochrome(false)` and no
size, so a baked mark is stuck at ONE colour on every theme and renders
at its source pixel count read as DIPs — a colour that never matches
the theme, and a size each control template clamps or does not.

Draw Lucide's own icon font instead. A FontIcon is sized by the control
and tinted from the foreground brush, exactly as the SymbolIcon it
replaced was: white on an accent button, the theme's colour in the
sidebar, and a vector at any DPI. The ring's discs take the same glyph
through a TextBlock, where the size is the ring's geometry and the ink
is white because a disc is dark on both themes.

Three PNG bakes, the two-colour split, the LOCALAPPDATA materializer
and its cache all go. What replaces them is a codepoint per icon in the
shared table and one 664 kB font.

The font reaches the exe by two routes, because ms-appx:/// resolves to
the install folder either way: build.rs stages it for a dev build, and
it is checked in under packaging/assets for a shipped one, which
pack-msix.ps1 copies into the layout the MSIX, the installer and the
portable zip are all packed from. That script now fails loudly if it is
missing — every icon would otherwise ship as a private-use box.
2026-08-31 00:18:19 +02:00
enricobuehler c2eea7d3cf fix(client/windows): the Lucide bake size IS the on-screen icon size
Every button's icon rendered enormous. Reactor's `.icon()` builds a
WinUI BitmapIcon and drops it into the control with no size set, and a
BitmapIcon measures at its source PIXEL count read as DIPs — where the
SymbolIcon it replaced self-sized to its glyph. The bake was 128 px, so
every button carried a 128 DIP mark.

It looked correct in the settings sidebar, which is what hid it: a
NavigationViewItem's template clamps its icon, an ordinary Button does
not.

Bake the button marks at 16 px instead, Segoe Fluent's optical weight
in a button. The ring keeps a large bake of its own, because it draws
through Image, which takes an explicit width and height — so a big
source there buys resolution rather than size.

The test asserts the ring bake stays much heavier than the button one:
collapsing the two back into a single bake is exactly the mistake, and
it is invisible until someone opens the app.
2026-08-31 00:07:43 +02:00
enricobuehler 3cfb3df594 fix(client/windows): the quick-action editor loses its gradient stage
The stage was a diagonal purple-to-teal gradient, baked to a BMP on
disk because reactor's brushes are flat. It read as a different app,
and the console's editor had already dropped its own for that reason.
It is now a flat card face, and the bitmap writer goes with it.

The discs carried short words and no icons at all. They now draw the
slot's Lucide mark from the shared table, the centre draws the ring's
own `more`, and the name of the disc under the pointer reads out in a
band below the ring — the band the other four editors already have,
and where the name went when the words became marks.

Reset stops being a drop-down menu labelled "Reset…" and becomes the
console's armed row: "Reset to default", then "Press again to reset",
with the line about what it removes under it. Same two presses, same
words as every other client.

The rest of the shell's icons move from Segoe Fluent symbols to the
same Lucide set. WinUI cannot tint them: reactor builds every
BitmapIcon with ShowAsMonochrome(false), so each mark ships baked in
two colours — mid-grey for ordinary surfaces, white for accent-filled
buttons and the ring's dark discs.
2026-08-30 23:44:43 +02:00
enricobuehler 2cc5dea786 fix(client/linux): the quick-action editor wears the ring's own marks
The editor is meant to BE the ring, but its discs carried short words
where the in-stream ring carries icons, and it sat on a purple-to-teal
gradient the console's editor had already dropped as decoration. Two
different pictures of one control.

Discs now draw the slot's Lucide mark from the shared table, the centre
draws the ring's own `more`, and the stage is a flat card face like
every other card in this shell.

The rest of the shell's icons move to Lucide too, so one page does not
sit in a different icon language from the page before it. GTK strokes
the same path data with gsk and cairo that Skia strokes on the console,
in the widget's own CSS colour — which is why a mark on a ring disc
comes out white without a second asset: `.pf-ring-disc` says so.

Three status-page illustrations keep their Adwaita names: AdwStatusPage
takes an icon name or a paintable, and a paintable cannot read the
widget colour without a GtkSymbolicPaintable subclass — more machinery
than three decorative empty states are worth.
2026-08-30 23:44:34 +02:00
enricobuehler a2d90ad047 feat(clients): one Lucide icon set for every shell to draw from
The console already drew Lucide marks, but only the console: its path
table was private to pf-console-ui and its slot-to-icon map lived in
ring.rs, so the two desktop editors that configure the same ring could
not reach either. The GTK editor drew words on its discs and the WinUI
editor drew words too — three shells, three vocabularies for one ring.

Move both tables down into pf-client-core, where every Rust client
already looks for the ring's model, and derive them from masters in
assets/lucide so a mark cannot drift between shells.

The console's icons.rs keeps aliases only for the marks its own chrome
draws. A slot's mark is no longer among them: the shared table hands
out a NAME, which `by_name` resolves — one lookup rather than an alias
per slot, and no second list to fall out of step with the first.

The icon test widens from the 19 marks the console named to all 34 the
workspace ships, because two more shells now stroke the same strings
and neither has a parser of its own to fail loudly.
2026-08-30 23:44:25 +02:00