From 56e161911e6fc833c3475abbfb3b7bb3dedd510a Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 3 Sep 2026 20:30:58 +0200 Subject: [PATCH] Make the save-password dialog actually visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing had ever been saved: every app carried savedAccount=None and the Keychain held no Work entries at all. So right-clicking "Fill saved password" had nothing to recall — it was never a fill bug. The offer was firing correctly on login; its dialog just could not be seen. Two things hid it. The stage was parked only for settingsOpen, so the password offer left the apps where they were — and a native webview paints over anything the shell draws. Every shell dialog needs the park, not just Settings. Parking alone was still not enough. show_only and set_stage each move every webview back to the stage, and the staggered bootstrap calls show_only once per app as it builds them, so the park was undone within milliseconds — the dialog appeared and vanished in a flash. Nothing recorded that the apps were *meant* to be away, so every layout pass raced to put them back. AppState now carries that intent and placement() applies it, so a pass that runs while a dialog is up leaves the apps parked instead of returning them to the stage. Verified end to end against a local login form: the offer appears and stays on an XHR login that fires no submit event, the credential reaches the Keychain, right-click fill puts it back, and a failed login is correctly not offered for saving. --- src-tauri/src/commands.rs | 10 ++++++++++ src-tauri/src/webviews.rs | 19 +++++++++++++++++-- src/App.tsx | 13 ++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f560542..08b9b52 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -48,6 +48,13 @@ pub struct AppState { pub unread: Mutex>, /// Size of the last dialog backdrop still, or why there wasn't one. pub last_snapshot: Mutex, + /// A dialog is up and the apps are parked off screen. + /// + /// Repositioning is not a one-off: webviews are re-laid-out whenever the + /// stage moves and once per app as the staggered bootstrap builds them. + /// Without somewhere to record that they are meant to be away, the next + /// such pass puts them straight back over the dialog. + pub parked: Mutex, } impl AppState { @@ -105,6 +112,7 @@ pub fn build_state(handle: &AppHandle) -> Result { last_notification: Mutex::new(String::new()), unread: Mutex::new(std::collections::HashMap::new()), last_snapshot: Mutex::new(String::new()), + parked: Mutex::new(false), }) } @@ -253,6 +261,7 @@ pub async fn stage_snapshot(app: AppHandle) -> Option { #[tauri::command] pub fn hide_stage(app: AppHandle, state: State<'_, AppState>) { let stage = *state.stage.lock().unwrap(); + *state.parked.lock().unwrap() = true; webviews::hide_all(&app, &state.cfg(), stage); } @@ -261,6 +270,7 @@ pub fn hide_stage(app: AppHandle, state: State<'_, AppState>) { pub fn show_stage(app: AppHandle, state: State<'_, AppState>) { let cfg = state.cfg(); let stage = *state.stage.lock().unwrap(); + *state.parked.lock().unwrap() = false; let active = state.active.lock().unwrap().clone(); webviews::show_only(&app, active.as_deref(), &cfg, stage); } diff --git a/src-tauri/src/webviews.rs b/src-tauri/src/webviews.rs index 063f1a4..ff05bf1 100644 --- a/src-tauri/src/webviews.rs +++ b/src-tauri/src/webviews.rs @@ -944,6 +944,19 @@ fn set_page_visibility(handle: &AppHandle, app_id: &str, visible: bool) { )); } +/// Where the apps belong right now. +/// +/// While a dialog is up they belong off screen, and every layout pass has to +/// agree — otherwise the pass that runs next puts them back over the dialog. +fn placement(handle: &AppHandle, stage: (f64, f64, f64, f64)) -> (f64, f64) { + let parked = *handle + .state::() + .parked + .lock() + .unwrap(); + (stage.0, if parked { stage.1 + PARKED_OFFSET } else { stage.1 }) +} + /// Brings one app to the front. Every other app stays live behind it. pub fn show_only( handle: &AppHandle, @@ -951,9 +964,10 @@ pub fn show_only( cfg: &Config, stage: (f64, f64, f64, f64), ) { + let (x, y) = placement(handle, stage); for app in &cfg.apps { let Some(wv) = handle.get_webview(&label_for(&app.id)) else { continue }; - let _ = wv.set_position(LogicalPosition::new(stage.0, stage.1)); + let _ = wv.set_position(LogicalPosition::new(x, y)); let _ = wv.set_size(LogicalSize::new(stage.2, stage.3)); let _ = wv.set_zoom(app.zoom); let _ = wv.show(); @@ -977,9 +991,10 @@ pub fn set_stage( radius: f64, ) { let cfg = handle.state::().cfg(); + let (x, y) = placement(handle, stage); for app in &cfg.apps { let Some(wv) = handle.get_webview(&label_for(&app.id)) else { continue }; - let _ = wv.set_position(LogicalPosition::new(stage.0, stage.1)); + let _ = wv.set_position(LogicalPosition::new(x, y)); let _ = wv.set_size(LogicalSize::new(stage.2, stage.3)); set_corner_radius(handle, &app.id, radius); } diff --git a/src/App.tsx b/src/App.tsx index 5ce0291..49c060a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -189,12 +189,19 @@ export default function App() { /* A native view paints over anything the shell draws, so a dialog needs the app moved out of the way rather than merely a higher z-index — and once it is moved there is nothing left behind the dialog to look at. A still taken - on the way out, blurred, puts the background back. */ + on the way out, blurred, puts the background back. + + This applies to EVERY shell dialog, not just settings. The save-password + offer once missed it and rendered behind the app's own view, where it was + both invisible and unclickable — so nothing was ever saved, and filling + had nothing to recall. */ + const dialogUp = settingsOpen || offer !== null; + useEffect(() => { if (!config) return; let cancelled = false; - if (settingsOpen) { + if (dialogUp) { void (async () => { const shot = await api.stageSnapshot().catch(() => null); if (cancelled) return; @@ -208,7 +215,7 @@ export default function App() { return () => { cancelled = true; }; - }, [settingsOpen, config]); + }, [dialogUp, config]); const toggleCollapse = () => { if (!config) return;