diff --git a/progress.txt b/progress.txt new file mode 100644 index 0000000..6a40075 --- /dev/null +++ b/progress.txt @@ -0,0 +1,8 @@ +## Codebase Patterns +- Native menu toggles are stored in AppState as CheckMenuItem handles; update the checkmark alongside persisted settings updates. +- PersistedState changes should be applied to new windows in apply_initial_window_state and saved via save_state after user actions. + +## Progress Log +- story-1: Added click-through menu toggle + shortcut with persistence, applied per-window at startup, and added Playwright coverage for the shortcut toggle. +- story-1: Re-ran checks; cargo check fails on lockfile v4 with cargo 1.75, cargo tauri CLI missing, Playwright browsers not installed. +- story-1: Verified click-through implementation remains intact; cargo check fails due to Cargo.lock v4 requiring -Znext-lockfile-bump, cargo tauri build --ci fails (cargo-tauri CLI missing), Playwright tests fail until `npx playwright install`. diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a02a7ed..375a428 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -28,9 +28,11 @@ enum WindowSizeUnits { } #[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(default)] struct PersistedState { last_file: Option, aspect_lock: bool, + click_through: bool, window_w: Option, window_h: Option, window_size_units: Option, @@ -66,6 +68,7 @@ struct AppState { aspect_ratio: Mutex>, // per-window aspect ratio adjusting_resize: Mutex>, // per-window resize guard aspect_toggle: Mutex>>, + click_through_toggle: Mutex>>, pending_save: Mutex>>, selections: Mutex>, // per-window selections last_focused_window: Mutex>, // label of last focused window @@ -121,6 +124,7 @@ impl Default for AppState { aspect_ratio: Mutex::new(HashMap::new()), adjusting_resize: Mutex::new(HashSet::new()), aspect_toggle: Mutex::new(None), + click_through_toggle: Mutex::new(None), pending_save: Mutex::new(HashMap::new()), selections: Mutex::new(HashMap::new()), last_focused_window: Mutex::new(None), @@ -188,6 +192,26 @@ fn save_state(app: &AppHandle, win: &WebviewWindow, mut st: PersistedState) -> R Ok(()) } +fn apply_click_through_setting(window: &WebviewWindow, enabled: bool) { + let _ = window.set_ignore_cursor_events(enabled); + let _ = window.set_always_on_top(true); +} + +fn update_click_through_state( + app: &AppHandle, + window: &WebviewWindow, + st: &mut PersistedState, + enabled: bool, +) { + st.click_through = enabled; + apply_click_through_setting(window, enabled); + if let Some(state) = app.try_state::() { + if let Some(toggle) = state.click_through_toggle.lock().clone() { + let _ = toggle.set_checked(enabled); + } + } +} + fn schedule_size_save(app: AppHandle, label: String, win: WebviewWindow) { if let Some(state) = app.try_state::() { let mut pending = state.pending_save.lock(); @@ -253,6 +277,9 @@ fn reset_cache(app: &AppHandle) -> Result<(), Error> { if let Some(toggle) = state.aspect_toggle.lock().clone() { let _ = toggle.set_checked(false); } + if let Some(toggle) = state.click_through_toggle.lock().clone() { + let _ = toggle.set_checked(false); + } } if let Ok(path) = config_path(app) { if path.exists() { @@ -397,6 +424,7 @@ fn apply_initial_window_state(app: &AppHandle, window: &WebviewWindow, load_last let _ = window.set_always_on_top(true); let st = load_state(app); + apply_click_through_setting(window, st.click_through); if let (Some(w), Some(h)) = (st.window_w, st.window_h) { let logical_size = match st.window_size_units.unwrap_or(WindowSizeUnits::Physical) { WindowSizeUnits::Logical => Some((w, h)), @@ -605,6 +633,7 @@ fn get_settings(app: AppHandle) -> PersistedState { #[derive(Deserialize)] struct SettingsUpdate { aspect_lock: Option, + click_through: Option, } #[tauri::command] @@ -623,6 +652,9 @@ fn set_settings(app: AppHandle, update: SettingsUpdate) -> Result() { *state.settings.lock() = st.clone(); @@ -758,6 +790,7 @@ fn main() { .manage(AppState::default()) .setup(|app| { let app_handle = app.handle().clone(); + let initial_state = load_state(&app_handle); // Build native menu with platform shortcuts and toggles. let file_menu = SubmenuBuilder::new(&app_handle, "File") @@ -808,9 +841,18 @@ fn main() { ) .build()?; + let click_through_toggle = + CheckMenuItemBuilder::with_id("click_through_toggle", "Click-Through") + .checked(initial_state.click_through) + .accelerator(if cfg!(target_os = "macos") { + "Cmd+Shift+C" + } else { + "Ctrl+Shift+C" + }) + .build(&app_handle)?; let aspect_toggle = CheckMenuItemBuilder::with_id("aspect_lock_toggle", "Lock aspect ratio on resize") - .checked(load_state(&app_handle).aspect_lock) + .checked(initial_state.aspect_lock) .build(&app_handle)?; let view_menu = SubmenuBuilder::new(&app_handle, "View") .item( @@ -840,6 +882,7 @@ fn main() { }) .build(&app_handle)?, ) + .item(&click_through_toggle) .item(&aspect_toggle); let app_menu = MenuBuilder::new(&app_handle) .item(&file_menu) @@ -848,10 +891,11 @@ fn main() { app.set_menu(app_menu)?; if let Some(state) = app_handle.try_state::() { *state.aspect_toggle.lock() = Some(aspect_toggle.clone()); + *state.click_through_toggle.lock() = Some(click_through_toggle.clone()); } if let Some(state) = app_handle.try_state::() { - *state.settings.lock() = load_state(&app_handle); + *state.settings.lock() = initial_state.clone(); state .window_counter .store(1, std::sync::atomic::Ordering::SeqCst); @@ -918,6 +962,21 @@ fn main() { let _ = navigate_selection(app, &win, 1); } } + "click_through_toggle" => { + if let Some(win) = focused_window(app) { + let mut s = if let Some(state) = app.try_state::() { + state.settings.lock().clone() + } else { + load_state(app) + }; + let next_state = !s.click_through; + update_click_through_state(app, &win, &mut s, next_state); + let _ = save_state(app, &win, s.clone()); + if let Some(state) = app.try_state::() { + *state.settings.lock() = s; + } + } + } "aspect_lock_toggle" => { if let Some(state) = app.try_state::() { let mut s = state.settings.lock().clone(); diff --git a/tests/tauri-driver.spec.ts b/tests/tauri-driver.spec.ts index d9b62aa..06c2444 100644 --- a/tests/tauri-driver.spec.ts +++ b/tests/tauri-driver.spec.ts @@ -43,6 +43,22 @@ const resolveDriverPath = (): string => { ); }; +const launchDriver = (iconPath: string) => { + const driverPath = resolveDriverPath(); + return spawn(driverPath, [], { + env: { ...process.env, FLOAT_TEST_PATH: iconPath }, + stdio: 'inherit', + }); +}; + +const getSettings = (page: import('@playwright/test').Page) => + page.evaluate(async () => { + const tauri = (window as any).__TAURI__ || {}; + if (tauri?.core?.invoke) return tauri.core.invoke('get_settings'); + if (tauri.invoke) return tauri.invoke('get_settings'); + return null; + }); + /** * Boot the Tauri app via tauri-driver and connect Playwright to it. * The driver listens on 5544 by default; we wait for readiness before connecting. @@ -53,11 +69,7 @@ test('opens app and shows toolbar', async ({ page }) => { throw new Error(`icon not found at ${iconPath}`); } - const driverPath = resolveDriverPath(); - const driver = spawn(driverPath, [], { - env: { ...process.env, FLOAT_TEST_PATH: iconPath }, - stdio: 'inherit', - }); + const driver = launchDriver(iconPath); // Give the driver time to start await new Promise((resolve) => setTimeout(resolve, 3000)); @@ -69,3 +81,39 @@ test('opens app and shows toolbar', async ({ page }) => { driver.kill(); }); + +test('toggles click-through with shortcut', async ({ page }) => { + const iconPath = path.resolve(__dirname, '..', 'src-tauri', 'icons', 'icon.png'); + if (!fs.existsSync(iconPath)) { + throw new Error(`icon not found at ${iconPath}`); + } + + const driver = launchDriver(iconPath); + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + await page.goto('http://localhost:5544/'); + await expect(page).toHaveTitle('Float'); + await page.waitForSelector('text=No file selected'); + + await page.click('body'); + + const shortcut = process.platform === 'darwin' ? 'Meta+Shift+C' : 'Control+Shift+C'; + + let settings = (await getSettings(page)) as any; + expect(settings?.click_through).toBeFalsy(); + + await page.keyboard.press(shortcut); + await page.waitForTimeout(200); + + settings = (await getSettings(page)) as any; + expect(settings?.click_through).toBe(true); + + await page.keyboard.press(shortcut); + await page.waitForTimeout(200); + + settings = (await getSettings(page)) as any; + expect(settings?.click_through).toBe(false); + + driver.kill(); +});