Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions progress.txt
Original file line number Diff line number Diff line change
@@ -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`.
63 changes: 61 additions & 2 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ enum WindowSizeUnits {
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
struct PersistedState {
last_file: Option<String>,
aspect_lock: bool,
click_through: bool,
window_w: Option<f64>,
window_h: Option<f64>,
window_size_units: Option<WindowSizeUnits>,
Expand Down Expand Up @@ -66,6 +68,7 @@ struct AppState {
aspect_ratio: Mutex<HashMap<String, f64>>, // per-window aspect ratio
adjusting_resize: Mutex<HashSet<String>>, // per-window resize guard
aspect_toggle: Mutex<Option<CheckMenuItem<Wry>>>,
click_through_toggle: Mutex<Option<CheckMenuItem<Wry>>>,
pending_save: Mutex<HashMap<String, async_runtime::JoinHandle<()>>>,
selections: Mutex<HashMap<String, SelectionState>>, // per-window selections
last_focused_window: Mutex<Option<String>>, // label of last focused window
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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::<AppState>() {
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::<AppState>() {
let mut pending = state.pending_save.lock();
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -605,6 +633,7 @@ fn get_settings(app: AppHandle) -> PersistedState {
#[derive(Deserialize)]
struct SettingsUpdate {
aspect_lock: Option<bool>,
click_through: Option<bool>,
}

#[tauri::command]
Expand All @@ -623,6 +652,9 @@ fn set_settings(app: AppHandle, update: SettingsUpdate) -> Result<PersistedState
}
}
}
if let Some(v) = update.click_through {
update_click_through_state(&app, &win, &mut st, v);
}
save_state(&app, &win, st.clone()).map_err(|e| e.to_string())?;
if let Some(state) = app.try_state::<AppState>() {
*state.settings.lock() = st.clone();
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -848,10 +891,11 @@ fn main() {
app.set_menu(app_menu)?;
if let Some(state) = app_handle.try_state::<AppState>() {
*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::<AppState>() {
*state.settings.lock() = load_state(&app_handle);
*state.settings.lock() = initial_state.clone();
state
.window_counter
.store(1, std::sync::atomic::Ordering::SeqCst);
Expand Down Expand Up @@ -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::<AppState>() {
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::<AppState>() {
*state.settings.lock() = s;
}
}
}
"aspect_lock_toggle" => {
if let Some(state) = app.try_state::<AppState>() {
let mut s = state.settings.lock().clone();
Expand Down
58 changes: 53 additions & 5 deletions tests/tauri-driver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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));
Expand All @@ -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();
});
Loading