Skin is a launch-time choice; stop applying it to a live window
Switching Modern -> Win7 at runtime broke the UI. SettingsService.applySkin toggled the `.win7` class the moment the setting changed — harmless when there was one component tree, and a broken window as soon as there were two. The tree is bootstrapped once from the preload-baked __APP_SKIN, so the live flip painted 7.css onto shadlike markup that has no .window.glass, no .title-bar, no menu[role=tablist] and no <table> ListView. The CSS landed on a tree that cannot receive it. A reload would not have helped, and neither would anything short of a restart: - The preload is a persistent WebKitUserScript injected at DOCUMENT_START, so its string is frozen at webview construction — loadURL() re-runs it with the OLD __APP_SKIN. updatePreloadScriptToWebView exists in the FFI but is reachable only from electrobun's internal <electrobun-webview> handler. - titleBarStyle/transparent have no setters; a live window cannot be re-framed. win7 -> modern is the dangerous direction — the modern tree draws no title bar, so it would land in a frameless transparent window with no way to move, minimize or close it. - Recreating the main window is also out: exitOnLastWindowClosed is true, the main rpc is captured by six push closures, the tray captures `win`, and listeners cannot be removed. So: applySkin is deleted outright, from setSkin AND from applyAll — the latter matters independently, since applyAll runs on the settingsChanged push and was therefore breaking every open editor and manager window too. Clicking a skin now only persists it. Also fixed a second leak found while tracing this: manager-window and editor-windows build from `currentSettings()`, the LIVE snapshot, so a window opened after a pending change would boot the new tree and frame while main still ran the old one. `initSessionSkin` now freezes the skin at startup before any window exists, and buildPreload/chromeOptions read that pinned value — chromeOptions loses its parameter so no call site can get it wrong. The options row shows the saved value as pending (skinPending() compares settings.skin() against APP_SKIN) with the hint retexted in all 7 locales; it said "Reopen the window to change its frame", which described the deleted design. Verified: tsc + build:ui clean; both skins boot correctly from a persisted setting (win7 frameless, modern with the native frame). The live flip is now impossible by construction, which is the check that matters here — the UI bundle contains no classList write for win7 at all, the only writer is buildPreload, and sessionSkin has exactly one assignment site. Not verified: the click itself and the pending hint appearing, which need an interaction pass — no window automation is available here. Both are present in the built bundle. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -64,7 +64,9 @@ export class SettingsService {
|
||||
this.autoClearCompleted.set(settings.autoClearCompleted);
|
||||
this.editor.set(settings.editor);
|
||||
this.editorApp.set(settings.editorApp);
|
||||
this.applySkin();
|
||||
// NO applySkin here, deliberately — see setSkin. applyAll also runs on the
|
||||
// settingsChanged push, so flipping the class here would break every OPEN
|
||||
// editor and manager window too, not just the one that changed the setting.
|
||||
this.applyTheme();
|
||||
this.applyDarkTone();
|
||||
this.applyAccent();
|
||||
@@ -79,15 +81,26 @@ export class SettingsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the CSS half of a skin change lands here. The component TREE and the
|
||||
* native frame are both fixed at window construction (main.ts reads the
|
||||
* preload-baked __APP_SKIN; titleBarStyle/transparent are BrowserWindow
|
||||
* constructor options), so the switch is not complete until the window is
|
||||
* reopened. The options row says so.
|
||||
* Saves the preference and NOTHING else — the skin is a launch-time choice.
|
||||
*
|
||||
* It deliberately does not touch the `.win7` class, which is the whole bug
|
||||
* this replaced: each skin's CSS is written against its OWN component tree's
|
||||
* markup, and the tree is fixed at bootstrap from the preload-baked
|
||||
* __APP_SKIN. Flipping the class live therefore painted 7.css onto shadlike
|
||||
* markup that has no .window.glass, no .title-bar, no menu[role=tablist] and
|
||||
* no <table> ListView — a broken window, not a reskinned one.
|
||||
*
|
||||
* A reload would not have helped either: the preload string is frozen at
|
||||
* webview construction (a persistent WebKitUserScript), and titleBarStyle /
|
||||
* transparent have no setters, so the native frame cannot change on a live
|
||||
* window. win7 -> modern without a reframe is the dangerous direction — the
|
||||
* modern tree draws no title bar, so it would land in a frameless window
|
||||
* with no way to move, minimize or close it.
|
||||
*
|
||||
* The options row shows the saved value as pending until the next start.
|
||||
*/
|
||||
setSkin(skin: AppSettings['skin']): void {
|
||||
this.skin.set(skin);
|
||||
this.applySkin();
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
@@ -158,12 +171,11 @@ export class SettingsService {
|
||||
void this.persist();
|
||||
}
|
||||
|
||||
/** A CLASS, not a data-* attribute: 7.scoped.css is precompiled to require a
|
||||
* literal `.win7` ancestor, so the name is not ours to choose. Mirrors what
|
||||
* buildPreload writes for the first paint. */
|
||||
private applySkin(): void {
|
||||
document.documentElement.classList.toggle('win7', this.skin() === 'win7');
|
||||
}
|
||||
// There is deliberately no applySkin(). The `.win7` class is written once by
|
||||
// buildPreload at DOCUMENT_START for the skin this window actually booted,
|
||||
// and must never change afterwards — see setSkin for why. Every other apply*
|
||||
// below is safe to run live because it only re-points tokens, and both skins
|
||||
// consume the same token system.
|
||||
|
||||
private applyTheme(): void {
|
||||
const theme = this.theme();
|
||||
|
||||
Reference in New Issue
Block a user