fix(plymouth): #137 splash follows the canvas; #145 keyboard hint on the LUKS prompt
All checks were successful
Check / eval (push) Successful in 3m59s

The logo was off-centre on the external when booting/shutting down docked.
Both earlier diagnoses in this repo were WRONG, and that is the part worth
keeping: Window.GetWidth() returns max_width (not head 0), and per-head
sprites would have DUPLICATED the logo — the script plugin already builds
one max_width x max_height canvas, centres every display inside it
(script-lib-sprite.c:536) and draws sprites at (sprite.x - display.x), so a
canvas-centred sprite is centred on every head. The arithmetic was right.

The bug was TIME: every position was a top-level statement evaluated once
at parse time. A head arriving/leaving mid-splash resizes the canvas, the
plugin re-centres the displays, and frozen sprites end up off by
(new_max - old_max)/2 on every head. One monitor never resizes the canvas —
hence "only when docked". Fix: one layout(), re-run from the existing
refresh callback on canvas change.

The trap that cost two attempts (now a comment): in plymouth script a bare
assignment inside a function writes the GLOBAL if that name already exists
globally. So `canvas_width = Window.GetWidth()` updated global.canvas_width
BEFORE the guard compared against it — always false, body never ran, splash
rendered as a bare background, NO error logged. Isolated with a 20-line
probe. Hence cw/ch.

#145 rides along: a VT loads one keymap and knows nothing of per-device
layouts, so the passphrase box types with the console layout — worth saying
before three wrong tries on a disk you cannot read (same gap as #114, one
step earlier). Fedora's mechanism does not port: ply_keymap_icon is a C
widget in the two-step plugin (the script plugin has no keyboard API), fed
from XKBLAYOUT in /etc/vconsole.conf, which NixOS never writes (plymouth's
trace says `XKBLAYOUT: (null)`). So @LAYOUT@ is baked from
services.xserver.xkb.layout like the palette — the FIRST of a comma list,
since that is what the VT loads. Icon is plymouth's own keyboard.png (the
same glyph Fedora shows), copied at build time so no GPL bytes enter the
repo, recoloured to subtext.

chmod +w after that copy: store files are 444 and recolor rewrites in
place. Without it magick fails, the phase aborts, and EVERY LATER SED
SILENTLY DOES NOT RUN — the theme shipped with @BG_R@ literals and I only
caught it by reading the build log.

Verified by render (tools/plymouth-preview.sh, real built theme): both
heads centred, password dialog showing padlock + entry + keyboard icon and
"us". V3 queued — only a real docked boot can resize a canvas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 13:14:05 +01:00
parent 121b91f69c
commit ff0f9d6359
6 changed files with 247 additions and 166 deletions

View File

@@ -177,129 +177,6 @@ migration (state.json created + git-staged, legacy `git rm`'d). Proven by
hand 2026-07-15 (drvPaths identical across the rename) — this just keeps it
proven. Delete together with the shim + the `nomarchy-theme-sync` alias.
### 145. Show the keyboard layout on the LUKS prompt (Fedora does this)
Bernardo 2026-07-16: the splash's password screen should say which layout is
loaded. It is the right ask for this machine specifically — a VT has **one**
keymap and no concept of per-device layouts, so `settings.keyboard.devices`
(his external board remembered as `us`) does **not** apply at the passphrase
box; it follows the session layout via `services.xserver.xkb.layout`
`console.useXkbConfig` (`modules/nixos/default.nix:96-102`). That is the same
structural gap as **#114** (tuigreet), one step earlier in the boot, and with
worse stakes: three wrong tries on a disk you cannot read yet, with no
indication that the keyboard is not the one you think.
**Fedora's mechanism does not port, and the reason matters.** Its label is
`ply_keymap_icon` — a **C widget in the two-step plugin**
(`plugins/splash/two-step/plugin.c:239`, `libply-splash-graphics/ply-keymap-icon.c`);
the **script** plugin has no keyboard API at all (nothing in
`script-lib-plymouth.c`). And it is fed from **`XKBLAYOUT` in
`/etc/vconsole.conf`**, which **NixOS does not write** — this box has exactly
`KEYMAP=/nix/store/…-xkb-console-keymap` and nothing else, so plymouth's own
trace says `XKBLAYOUT: (null)`. Switching to a C-plugin theme *and* teaching
NixOS to emit XKBLAYOUT to get a stock feature is a bad trade for a text label.
**So bake it, the way the palette already is.** The theme derivation seds
`@BG_R@/@BG_G@/@BG_B@` into `nomarchy.script` at build time
(`modules/nixos/plymouth.nix`); add `@LAYOUT@` from
`config.nomarchy.keyboard.layout` and draw it under the entry with
`Image.Text` (already used by `display_message_callback`), in the muted/subtext
colour, only inside `display_password_callback`. It stays honest because the
label and the console keymap are baked from the same option in the same
rebuild.
To settle while building: (a) `keyboard.layout` may be a comma list (`us,gb`) —
the VT loads the **first**, so print that one, not the raw string, or the label
lies in exactly the situation it exists for; (b) wording — Fedora says
"keyboard layout: us"; (c) whether to render it at all when there is only one
layout and no remembered devices (an argument for always: the point is to tell
you what the *console* does, which is not what your session does).
**Do #137 first, and land this with it:** the label is one more sprite that has
to move when the canvas resizes, so it belongs in that item's `layout()`, and
both are proven by the same `tools/plymouth-preview.sh` run — which renders
`ask-for-password`, i.e. exactly this screen.
### 137. Plymouth splash draws on head 0's geometry, so it is off-centre when docked
Bernardo 2026-07-16 (T14s, docked): the boot **and** shutdown logo is not
centred on the external monitor.
**The first diagnosis here was wrong — do not act on it.** It claimed
`Window.GetWidth()` returns head 0 and that the fix was per-head sprites. Both
are false, and per-head sprites would have been a *regression* (duplicate
logos). Read from plymouth's own source (`plymouth.src`, the build nixpkgs
ships) rather than from a summary:
- `Window.GetWidth()` **without an index returns `data->max_width`**
(`script-lib-sprite.c:227`) — the widest head, not head 0.
- The script plugin builds a virtual canvas of `max_width × max_height` and
**centres every display inside it**: `display->x = (max_width - width) / 2`
(`:536`). Sprites are drawn per display at `sprite->x - display->x` (`:452`).
- Therefore a canvas-centred sprite **is** centred on every head, and today's
`Window.GetWidth()/2 - w/2` is *correct at the instant it runs*. Note the DRM
renderer sets every head's `area.x = 0` (`renderers/drm/plugin.c:612`) — the
origins that matter are the plugin's computed ones, not the crtc's.
**The real bug is time, not geometry.** `nomarchy.script` computes every
position as a **top-level statement at parse time** (`:9`, `:1516`, `:129`,
`:210`, `:219`), i.e. once, against whatever canvas existed at that moment.
When a display arrives or leaves *during* the splash — which is exactly
"booting up / shutting down **with** an external" — `max_width/max_height`
change, the plugin re-centres each display (`:532-537`), but the sprite's frozen
canvas coordinate does not move with it. Every element then sits off by
`(new_max - old_max) / 2` **on every head**. A single monitor never changes the
canvas, which is precisely why this has never been seen undocked.
**Fix shape:** hoist the position math into a `reposition()` and call it from
the existing `refresh_callback` (already running at 50 fps) whenever
`Window.GetWidth()/GetHeight()` differ from the last canvas — logo (scale
included), entry, lock, any live bullets, progress box/bar. Keep it a
canvas-change check, not an every-frame recompute.
**The rig works, needs no root, and is in `scratchpad/ply-rig.sh`** (recipe
below — rebuild it, it is ~20 lines). Rendered on the dev box 2026-07-16:
- `unshare -rm` gives a user + mount namespace: root *in there* only, every
mount invisible to the real system and gone when it exits.
- `/run/plymouth` is BOTH the daemon's socket dir **and** its compiled-in
plugin/theme path (`strings plymouthd``/run/plymouth/plugins/`). Bind a
writable dir over it **and recreate its three symlinks** (`plugins`,
`themes`, `plymouthd.defaults``/etc/plymouth/…`) or plymouth cannot find
`x11.so`, silently skips X11 and reaches for a tty.
- **Do not pass `--tty`**: `main.c:2133` only takes the X11 path while
`default_tty` is unset. Bind an empty dir over `/dev/dri` too — with X11
first in the renderer order (`ply-renderer.c:283`) DRM is never used, and the
mask makes that a guarantee rather than a hope.
- `PLY_CREATE_FAKE_MULTI_HEAD_SETUP=1` → 800×600 + 640×480 windows.
`plymouth show-splash`, then `plymouth ask-for-password` renders the LUKS
dialog. Debug: `--kernel-command-line="plymouth.debug=stream:/path splash"`
(**`stream:`**, not `file:``file:` buffers and you get nothing).
- Fast loop: copy the theme into the rig's `themes/` and sed its `.plymouth`
`ScriptFile=`/`ImageDir=` to the copy — then editing the script is a 10s
render instead of a 3min `nixos-rebuild`.
**What the rig already proved (2026-07-16):** with a *stable* two-head canvas
the **current** theme centres correctly on both heads — logo and progress bar
each in their own head's middle. So the geometry is not the bug, which is the
evidence for the timing diagnosis above. The fake path builds both heads up
front, so it cannot reproduce a canvas *change*: for that, plug/unplug while
the splash is up, or compare a boot with and without the external.
**Attempt 1 failed, and the rig caught it — that is the point.** Hoisting the
positions into a `layout()` called from `refresh_callback` rendered the
background on both heads and **nothing else**: no logo, no progress bar. The
script plugin logs `starting boot animation` and **no parse error**, so it
fails *silently* — reverted rather than shipped (that script draws the LUKS box
on an encrypted machine). Next session: bisect with the fast loop. Prime
suspect is scope — plymouth script assigns into `local` unless the name already
exists, so a `logo.image = …` inside a function may not be the `logo` the rest
of the script sees, even though the same pattern works in `update_progress_bar`.
Test that hypothesis *first*, in isolation, before rewriting anything.
Pass = with the rig, a two-head render centred on both **plus the password
dialog visible**; then a real docked boot + shutdown, undocked unchanged.
### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry
Bernardo 2026-07-16, and his repro is exact: open the app menu, type `ca`, move

View File

@@ -509,6 +509,17 @@ Everything else below stays open; order is convenience, not a gate.
active and PPD switching governors; opt-ins: ROCm (`rocminfo`, a GPU
PyTorch/Ollama smoke) and the XDNA NPU driver loading.
- [ ] **Fingerprint**`fprintd-enroll` + (opt-in PAM) login/sudo.
- [ ] **#137/#145 splash on a real docked boot** — the one thing the render rig
cannot do: its fake heads exist from the start, so the canvas never
resizes, which *is* the bug. **Docked**, reboot: pass = the logo is centred
on **both** panels at boot **and** at shutdown (it was off-centre on the
external), and the LUKS prompt shows the padlock, the entry, and beneath it
a keyboard icon + your layout (`us`). Then reboot **undocked** — unchanged.
Rendering is already proven (`tools/plymouth-preview.sh`, both heads +
`ask-for-password`), so what is unproven is only a canvas that changes
under a real DRM boot. If the splash is ever blank, the passphrase prompt
is invisible but still live — type it blind, or pick the previous
generation in the boot menu, which carries the old theme.
- [ ] **#142 dock mode survives a rebuild** — **relogin first** (the watcher is
`exec-once`; `nomarchy-home` alone leaves the old one running and you
would be testing nothing — round 6's lesson). Then, docked: run

View File

@@ -19,6 +19,35 @@ Template:
---
## 2026-07-16 — #137 + #145: the splash follows the canvas, and says your layout
- **Task:** #137 (splash off-centre docked) + #145 (keyboard layout on the LUKS
prompt, with Fedora's icon).
- **Did:** one `layout()`, re-run from the existing refresh callback on canvas
change; `@LAYOUT@` + `@FG_*@` baked by plymouth.nix like the palette already
is; plymouth's own `keyboard.png` copied at build time and recoloured.
- **The lesson, and it is the whole entry:** I was wrong twice, and only the
render rig caught it. Not head-0 geometry (`Window.GetWidth()` = `max_width`);
not per-head sprites (would have duplicated the logo — the plugin already
centres each display in the canvas). The bug was **time**: positions computed
once at parse time vs a canvas that resizes when a head arrives. And attempt 1
of the fix died on a plymouth-script trap: **a bare assignment inside a
function writes the global if that name exists globally**, so
`canvas_width = Window.GetWidth()` updated the very global its guard compared
against — always false, nothing drawn, **no error logged**. Isolated with a
20-line probe (`[after assign: global.canvas_width=800][guard SKIPPED]`).
Both facts are now comments in the script; do not re-derive them.
- **Verified:** V0 + V2 by render — `tools/plymouth-preview.sh` with the real
built theme: both heads centred, and the password dialog showing padlock,
entry, and ⌨ + `us`. Also caught, only because I read the build log: copying a
file from the store keeps it **444**, so `recolor` failed, the phase aborted,
and **every later sed silently did not run** — the theme shipped with `@BG_R@`
literals. `chmod +w`. Check build logs, not just exit codes.
- **Pending:** V3 queued — a real docked boot/shutdown is the only way to resize
a canvas (the rig's fake heads exist from the start). Recovery if the splash
is ever blank: the prompt is invisible but live (type blind), or boot the
previous generation.
- **Next suggestion:** #129/#130 (GTK contrast) or #133 (the #107 shim check).
## 2026-07-16 — "fix the rest": #140, #142, #144 shipped; #137 re-diagnosed
- **Task:** the T14s round's remainder. Bernardo picked configurable-provider +
drop-the-CLI for #140 (and asked for chatgpt/grok in the list).

View File

@@ -449,6 +449,49 @@ Design/decision records and a running log of shipped work (items marked
(#131). Those windows therefore carry float/center rules and deliberately no
`size` rule. Measured after: calendar 1536×936 = exactly 60%×65%, doctor
1408×1008 = 55%×70%, both centred in the Waybar work area.
- ✓ **Splash: positions follow the canvas, + a keyboard hint on the LUKS prompt
(#137/#145, 2026-07-16):** Bernardo's logo was off-centre on the external when
booting/shutting down docked. **The first two diagnoses were both wrong**, and
the record matters more than the fix: it is *not* that `Window.GetWidth()`
returns head 0 (it returns `max_width`), and *not* that per-head sprites were
needed (they would have duplicated the logo). The script plugin builds one
canvas of `max_width × max_height`, **centres every display inside it**
(`display->x = (max_width - width)/2`, `script-lib-sprite.c:536`) and draws
each sprite at `sprite.x - display.x` — so a canvas-centred sprite is centred
on **every** head and the theme's arithmetic was right all along. The bug was
**time**: every position was a top-level statement evaluated once at parse
time, so a head arriving or leaving mid-splash resized the canvas, the plugin
re-centred the displays, and the frozen sprites ended up off by
`(new_max - old_max)/2` on every head. One monitor never resizes the canvas —
hence "only when docked". Fix: one `layout()`, re-run from the existing
refresh callback on canvas change.
**The trap that cost two attempts, now a comment in the script:** in plymouth
script a bare assignment inside a function writes the **global** if that name
already exists globally (`global.foo` and `foo` are one variable). So
`canvas_width = Window.GetWidth()` updated `global.canvas_width` *before* the
guard compared against it — always false, body never ran, whole splash a bare
background, **no error logged**. Proven with a 20-line probe:
`[after assign: global.canvas_width=800][guard SKIPPED]` vs a distinctly named
local firing correctly. Hence `cw`/`ch`.
**#145** rides along: a VT loads exactly one keymap and knows nothing of
per-device layouts, so the passphrase box types with the console layout —
worth stating before three wrong tries on a disk nobody can read yet (the same
gap as #114, one step earlier in the boot). Fedora's own mechanism does not
port: its label is `ply_keymap_icon`, a **C widget** in the two-step plugin
(the script plugin has no keyboard API), fed from `XKBLAYOUT` in
`/etc/vconsole.conf` — which **NixOS never writes** (this box has only
`KEYMAP=<store path>`; plymouth's trace literally says `XKBLAYOUT: (null)`).
So `@LAYOUT@` is baked from `services.xserver.xkb.layout` exactly like the
palette already is — **first** of a comma list, because that is what the VT
loads and the raw string would lie in the multi-layout case the label exists
for. The icon is plymouth's own `keyboard.png` (literally Federa's glyph),
copied at build time so no GPL bytes enter this repo, recoloured to subtext.
`chmod +w` after that copy: store files are 444 and `recolor` rewrites in
place — without it magick fails, the phase aborts, **every later sed silently
does not run**, and the theme ships with `@BG_R@` in it.
V2 by render, both heads + the password dialog, via the new
`tools/plymouth-preview.sh`. V3 (a real docked boot/shutdown) is queued: the
fake heads exist from the start, so the rig cannot resize a canvas.
- ✓ **Dock mode survives a rebuild (#142, 2026-07-16):** Bernardo: every rebuild
while docked re-lit the laptop panel and threw a workspace onto it, and he
re-picked Dock mode by hand. It was not forgetfulness — **a rebuild performed

View File

@@ -24,8 +24,20 @@ let
text = colorOf "text" "#D3DAE0";
surface = colorOf "surface" "#303A46";
accent = colorOf "accent" "#B79BE8";
# The keyboard hint is a footnote, not a headline: subtext, like the rest of
# the palette's secondary text (#145).
subtext = colorOf "subtext" "#97A3B2";
magick = lib.getExe' pkgs.imagemagick "magick";
# What the passphrase prompt actually types with. `services.xserver.xkb.layout`
# is this distro's single source for the layout, bridged to the console (and
# so to the initrd prompt) by console.useXkbConfig — see modules/nixos/default.nix.
# A VT loads exactly ONE keymap, so a comma list ("us,gb") means the FIRST:
# printing the raw string would lie in precisely the multi-layout case the
# label exists for.
kbdLayout = lib.head
(lib.splitString "," (config.services.xserver.xkb.layout or "us"));
# Plymouth's Window.SetBackgroundTopColor takes three floats in
# 0.01.0; the .plymouth metadata's ConsoleLogBackgroundColor takes a
# 0xRRGGBB hex. Nix has no float math: multiply, integer-divide, pad.
@@ -38,7 +50,9 @@ let
else if lib.stringLength s == 2 then "0${s}"
else s;
in "0.${padded}";
channel = off: byteToFloat (lib.fromHexString (lib.substring off 2 base));
channelOf = hex: off: byteToFloat (lib.fromHexString (lib.substring off 2 hex));
channel = channelOf base; # Window.SetBackground* (the splash base)
fgChannel = channelOf subtext; # Image.Text needs the same 0.0-1.0 floats
nomarchy-plymouth = pkgs.stdenv.mkDerivation {
pname = "nomarchy-plymouth";
@@ -53,6 +67,17 @@ let
mkdir -p "$themedir"
cp * "$themedir/"
# The keyboard glyph for the passphrase hint (#145) is plymouth's own
# literally the icon Fedora shows, since its keymap widget loads this same
# asset. Copied at build time rather than vendored: the bytes stay in
# nixpkgs' (GPL) plymouth and never enter this repo, and it cannot drift
# from the plymouth we actually run.
# chmod: store files are read-only (444) and `recolor` rewrites in place.
# unpackPhase makes the *source* writable, which is why the art below
# needs no such thing a file copied straight from the store does.
cp ${pkgs.plymouth}/share/plymouth/themes/spinner/keyboard.png "$themedir/keyboard.png"
chmod +w "$themedir/keyboard.png"
# Recolor the splash art from the palette (flat fill, alpha kept) so
# it reads on any base instead of the shipped fixed navy.
recolor() { ${magick} "$themedir/$1" -fill "#$2" -colorize 100 "$themedir/$1"; }
@@ -62,6 +87,7 @@ let
recolor entry.png ${surface} # password field box
recolor progress_box.png ${surface} # progress track
recolor progress_bar.png ${accent} # progress fill
recolor keyboard.png ${subtext} # passphrase keyboard-layout hint
# Point the .plymouth metadata into the store
sed -i "s|/usr/share/plymouth/themes/nomarchy|$themedir|g" \
@@ -73,6 +99,10 @@ let
-e 's|@BG_R@|${channel 0}|g' \
-e 's|@BG_G@|${channel 2}|g' \
-e 's|@BG_B@|${channel 4}|g' \
-e 's|@FG_R@|${fgChannel 0}|g' \
-e 's|@FG_G@|${fgChannel 2}|g' \
-e 's|@FG_B@|${fgChannel 4}|g' \
-e 's|@LAYOUT@|${kbdLayout}|g' \
"$themedir/nomarchy.script"
sed -i 's|@BG_HEX@|${base}|g' \
"$themedir/nomarchy.plymouth"

View File

@@ -3,18 +3,125 @@
Window.SetBackgroundTopColor(@BG_R@, @BG_G@, @BG_B@);
Window.SetBackgroundBottomColor(@BG_R@, @BG_G@, @BG_B@);
logo.image = Image("logo.png");
# Everything is placed by layout(), which runs on every canvas change — never
# once at parse time (#137).
#
# How the canvas works, because it is not obvious and the old code read as
# correct: the script plugin lays all heads out in ONE virtual canvas of
# max_width x max_height (the largest head), centres each display inside it
# (display->x = (max_width - width) / 2) and draws each sprite at
# (sprite.x - display.x). So Window.GetWidth() is the CANVAS width — the
# widest head, NOT head 0 — and a canvas-centred sprite lands centred on every
# head. The arithmetic below is unchanged from the original and it was always
# right. What it was not is permanent: when a head arrives or leaves the
# canvas resizes, the plugin re-centres each display, and sprites keep the
# coordinates they were given — so positions frozen at parse time end up off by
# (new_max - old_max) / 2 on EVERY head. Booting or shutting down with an
# external attached is exactly that. One monitor never resizes the canvas,
# which is why this only ever showed docked.
#
# TRAP — read before editing layout(). In plymouth script a bare assignment
# inside a function writes the GLOBAL if that name already exists globally
# (`global.foo` and a bare `foo` are the same variable; it only becomes a local
# when no global of that name exists). So a local named after the global it
# guards on silently updates that global *before* the comparison, the guard is
# then always false, and the body never runs — the whole splash renders as a
# bare background, with no error logged anywhere. Hence `cw`/`ch` below, and
# never `canvas_width = Window.GetWidth()`.
logo.original_image = Image("logo.png");
lock.image = Image("lock.png");
entry.image = Image("entry.png");
bullet.image = Image("bullet.png");
# The keyboard hint for the passphrase prompt (#145): a VT loads ONE keymap and
# knows nothing of per-device layouts, so what you type here is the console
# layout — which is worth saying out loud before three wrong tries on a disk
# nobody can read yet. Both are baked at build time by plymouth.nix.
kbd.icon_image = Image("keyboard.png");
kbd.text_image = Image.Text("@LAYOUT@", @FG_R@, @FG_G@, @FG_B@);
# Calculate scale factor to make logo ~15% of screen height
logo_scale_factor = (Window.GetHeight() * 0.15) / logo.image.GetHeight();
logo_width = logo.image.GetWidth() * logo_scale_factor;
logo_height = logo.image.GetHeight() * logo_scale_factor;
logo.image = logo.image.Scale(logo_width, logo_height);
logo.sprite = Sprite(logo.image);
logo.sprite.SetX (Window.GetWidth() / 2 - logo.image.GetWidth() / 2);
logo.sprite.SetY (Window.GetHeight() / 2 - logo.image.GetHeight() / 2);
logo.sprite = Sprite();
logo.sprite.SetOpacity (1);
entry.sprite = Sprite(entry.image);
entry.sprite.SetOpacity (0);
lock.sprite = Sprite();
lock.sprite.SetOpacity (0);
kbd.icon_sprite = Sprite();
kbd.icon_sprite.SetOpacity (0);
kbd.text_sprite = Sprite();
kbd.text_sprite.SetOpacity (0);
global.canvas_width = 0;
global.canvas_height = 0;
global.bullet_size = 7;
global.bullet_gap = 5;
fun layout ()
{
cw = Window.GetWidth();
ch = Window.GetHeight();
if (cw != global.canvas_width || ch != global.canvas_height)
{
global.canvas_width = cw;
global.canvas_height = ch;
# Logo: ~15% of canvas height, centred.
logo_scale = (ch * 0.15) / logo.original_image.GetHeight();
logo.image = logo.original_image.Scale(
logo.original_image.GetWidth() * logo_scale,
logo.original_image.GetHeight() * logo_scale);
logo.sprite.SetImage(logo.image);
logo.sprite.SetX(cw / 2 - logo.image.GetWidth() / 2);
logo.sprite.SetY(ch / 2 - logo.image.GetHeight() / 2);
# Password entry, under the logo.
entry.x = cw / 2 - entry.image.GetWidth() / 2;
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
entry.sprite.SetPosition(entry.x, entry.y, 10001);
# Lock, slightly shorter than the entry, to its left.
# (source lock.png is 84x96)
lock_h = entry.image.GetHeight() * 0.8;
lock_w = 84 * (lock_h / 96);
lock.sprite.SetImage(lock.image.Scale(lock_w, lock_h));
lock.sprite.SetPosition(entry.x - lock_w - 15,
entry.y + entry.image.GetHeight() / 2 - lock_h / 2,
10001);
# Keyboard hint, centred as one icon+text group under the entry.
kbd_gap = 8;
kbd_x = cw / 2 - (kbd.icon_image.GetWidth() + kbd_gap
+ kbd.text_image.GetWidth()) / 2;
kbd_y = entry.y + entry.image.GetHeight() + 18;
kbd.icon_sprite.SetImage(kbd.icon_image);
kbd.icon_sprite.SetPosition(kbd_x, kbd_y, 10001);
kbd.text_sprite.SetImage(kbd.text_image);
kbd.text_sprite.SetPosition(
kbd_x + kbd.icon_image.GetWidth() + kbd_gap,
kbd_y + kbd.icon_image.GetHeight() / 2 - kbd.text_image.GetHeight() / 2,
10001);
# Bullets already on screen belong to the old canvas.
for (index = 0; bullet.sprites[index]; index++)
{
bullet.sprites[index].SetPosition(
entry.x + 20 + index * (global.bullet_size + global.bullet_gap),
entry.y + entry.image.GetHeight() / 2 - global.bullet_size / 2,
10002);
}
# Progress box + bar share the entry's line.
progress_box.sprite.SetPosition(
cw / 2 - progress_box.image.GetWidth() / 2,
entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2,
0);
progress_bar.sprite.SetPosition(
cw / 2 - progress_bar.original_image.GetWidth() / 2,
entry.y + entry.image.GetHeight() / 2
- progress_bar.original_image.GetHeight() / 2,
1);
}
}
# Use these to adjust the progress bar timing
global.fake_progress_limit = 0.7; # Target percentage for fake progress (0.0 to 1.0)
@@ -31,6 +138,10 @@ global.max_progress = 0.0; # Track the maximum progress reached to prevent back
fun refresh_callback ()
{
# Cheap: two Window.Get*() reads; layout() returns at once unless the canvas
# actually resized (a head arrived or left).
layout();
global.animation_frame++;
# Animate fake progress to limit over time with easing
@@ -91,12 +202,18 @@ fun show_password_dialog()
{
lock.sprite.SetOpacity(1);
entry.sprite.SetOpacity(1);
# The keyboard hint belongs to the prompt: it is only ever the answer to
# "what am I typing with?", so it appears and leaves with the box (#145).
kbd.icon_sprite.SetOpacity(1);
kbd.text_sprite.SetOpacity(1);
}
fun hide_password_dialog()
{
lock.sprite.SetOpacity(0);
entry.sprite.SetOpacity(0);
kbd.icon_sprite.SetOpacity(0);
kbd.text_sprite.SetOpacity(0);
for (index = 0; bullet.sprites[index]; index++)
bullet.sprites[index].SetOpacity(0);
}
@@ -121,30 +238,7 @@ fun stop_fake_progress()
#----------------------------------------- Dialogue --------------------------------
lock.image = Image("lock.png");
entry.image = Image("entry.png");
bullet.image = Image("bullet.png");
entry.sprite = Sprite(entry.image);
entry.x = Window.GetWidth()/2 - entry.image.GetWidth() / 2;
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
entry.sprite.SetPosition(entry.x, entry.y, 10001);
entry.sprite.SetOpacity(0);
# Scale lock to be slightly shorter than entry field height
# Original lock is 84x96, entry height determines scale
lock_height = entry.image.GetHeight() * 0.8;
lock_scale = lock_height / 96;
lock_width = 84 * lock_scale;
scaled_lock = lock.image.Scale(lock_width, lock_height);
lock.sprite = Sprite(scaled_lock);
lock.x = entry.x - lock_width - 15;
lock.y = entry.y + entry.image.GetHeight()/2 - lock_height/2;
lock.sprite.SetPosition(lock.x, lock.y, 10001);
lock.sprite.SetOpacity(0);
# Bullet array
# Images and sprites are created at the top; every position lives in layout().
bullet.sprites = [];
fun display_normal_callback ()
@@ -206,21 +300,18 @@ Plymouth.SetDisplayPasswordFunction(display_password_callback);
progress_box.image = Image("progress_box.png");
progress_box.sprite = Sprite(progress_box.image);
progress_box.x = Window.GetWidth() / 2 - progress_box.image.GetWidth() / 2;
progress_box.y = entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2;
progress_box.sprite.SetPosition(progress_box.x, progress_box.y, 0);
progress_box.sprite.SetOpacity(0);
progress_bar.original_image = Image("progress_bar.png");
progress_bar.sprite = Sprite();
progress_bar.image = progress_bar.original_image.Scale(1, progress_bar.original_image.GetHeight());
progress_bar.x = Window.GetWidth() / 2 - progress_bar.original_image.GetWidth() / 2;
progress_bar.y = progress_box.y + (progress_box.image.GetHeight() - progress_bar.original_image.GetHeight()) / 2;
progress_bar.sprite.SetPosition(progress_bar.x, progress_bar.y, 1);
progress_bar.sprite.SetOpacity(0);
# First placement: everything layout() reads now exists. The refresh callback
# re-runs it, so a head arriving or leaving mid-splash moves the splash with it
# instead of stranding it against a canvas that is gone.
layout();
fun progress_callback (duration, progress)
{
global.real_progress = progress;