Skip to content
Open
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
5 changes: 4 additions & 1 deletion apps/desktop/src-tauri/src/general_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ impl MainWindowRecordingStartBehaviour {
#[cfg(windows)]
return window.minimize();
#[cfg(not(windows))]
window.hide()
{
crate::hide_main_window(window.app_handle());
Ok(())
}
}
Self::Minimise => window.minimize(),
}
Expand Down
36 changes: 26 additions & 10 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,12 @@ async fn get_devices_snapshot() -> DevicesUpdated {
}
}

fn any_webview_window_visible(app: &AppHandle) -> bool {
app.webview_windows()
.values()
.any(|window| window.is_visible().unwrap_or(false))
}

fn spawn_devices_snapshot_emitter(app_handle: AppHandle) {
tokio::spawn(async move {
let mut last_perm_tuple: (u8, u8, u8, u8) = (255, 255, 255, 255);
Expand All @@ -1766,6 +1772,14 @@ fn spawn_devices_snapshot_emitter(app_handle: AppHandle) {
continue;
}

// Device snapshots only feed UI pickers via DevicesUpdated, so
// polling while every window sits hidden in the tray probes the
// OS device stack and burns CPU for nobody (#2132).
if !any_webview_window_visible(&app_handle) {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
continue;
}

let permissions = permissions::do_permissions_check(false);
let Some((cameras, microphones)) = collect_device_inventory(
|| app_is_exiting(&app_handle),
Expand Down Expand Up @@ -4739,9 +4753,7 @@ pub async fn open_target_picker(
) {
use tauri::Manager;

if let Some(window) = CapWindowId::Main.get(app) {
window.hide().ok();
}
hide_main_window(app);

let state = app.state::<target_select_overlay::WindowFocusManager>();
let display_id = None;
Expand Down Expand Up @@ -5644,7 +5656,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
}
CapWindowId::Main => {
api.prevent_close();
let _ = window.hide();
hide_main_window(app);

#[cfg(target_os = "macos")]
crate::permissions::schedule_macos_dock_visibility_sync(app);
Expand Down Expand Up @@ -6798,9 +6810,15 @@ fn show_import_error_dialog(app: &AppHandle, message: String) {
.show(|_| {});
}

fn hide_main_window(app: &AppHandle) {
if let Some(main_window) = CapWindowId::Main.get(app) {
let _ = main_window.hide();
// Hidden webviews on Windows never see document.visibilityState change
// (tauri-apps/tauri#9524), so the frontend cannot detect hide-to-tray on its
// own; this event lets it pause polling, and only fires when the hide
// actually happened so a failed hide never pauses a visible window (#2132).
pub(crate) fn hide_main_window(app: &AppHandle) {
if let Some(main_window) = CapWindowId::Main.get(app)
&& main_window.hide().is_ok()
{
let _ = main_window.emit_to(CapWindowId::Main.label(), "main-window-hidden", ());
}
}

Expand Down Expand Up @@ -6880,9 +6898,7 @@ fn open_project_from_path(path: &Path, app: AppHandle) -> Result<(), String> {
let _ = app
.opener()
.open_path(mp4_path.to_str().unwrap_or_default(), None::<String>);
if let Some(main_window) = CapWindowId::Main.get(&app) {
main_window.hide().ok();
}
hide_main_window(&app);
}
}
}
Expand Down
16 changes: 6 additions & 10 deletions apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ fn hide_recording_windows(app: &AppHandle, restore_target_select_overlays: bool)
focus_manager.remember_overlay_for_restore(label);
}
hide_overlay(&window);
} else if matches!(id, CapWindowId::Main) {
crate::hide_main_window(app);
} else {
let _ = window.hide();
}
Expand Down Expand Up @@ -1515,7 +1517,7 @@ impl ShowCapWindow {
init_target_mode: Some(target_mode),
} = self
{
window.hide().ok();
crate::hide_main_window(app);
emit_app_event(
app,
RequestSetTargetMode {
Expand Down Expand Up @@ -2044,9 +2046,7 @@ impl ShowCapWindow {
window
}
Self::Upgrade => {
if let Some(main) = CapWindowId::Main.get(app) {
let _ = main.hide();
}
crate::hide_main_window(app);

let window = self
.window_builder(app, "/upgrade")
Expand Down Expand Up @@ -2080,9 +2080,7 @@ impl ShowCapWindow {
window
}
Self::ModeSelect => {
if let Some(main) = CapWindowId::Main.get(app) {
let _ = main.hide();
}
crate::hide_main_window(app);

let window = self
.window_builder(app, "/mode-select")
Expand Down Expand Up @@ -2116,9 +2114,7 @@ impl ShowCapWindow {
window
}
Self::Onboarding => {
if let Some(main) = CapWindowId::Main.get(app) {
let _ = main.hide();
}
crate::hide_main_window(app);

let width = (cursor_monitor.width * 0.58).clamp(860.0, 1080.0);
let height = (width * 0.72).clamp(690.0, 780.0);
Expand Down
47 changes: 46 additions & 1 deletion apps/desktop/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Route, Router, useCurrentMatches } from "@solidjs/router";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import {
focusManager,
QueryClient,
QueryClientProvider,
} from "@tanstack/solid-query";
import {
getCurrentWebviewWindow,
type WebviewWindow,
Expand Down Expand Up @@ -124,6 +128,7 @@ export default function App() {
function Inner() {
const currentWindow = getCurrentWebviewWindow();
createThemeListener(currentWindow);
createHiddenWindowQueryPause(currentWindow);

onMount(() => {
initAnonymousUser();
Expand Down Expand Up @@ -288,6 +293,46 @@ function prewarmFontCaches() {
else setTimeout(warm, 250);
}

// Hidden Tauri windows never flip document.visibilityState on Windows
// (tauri-apps/tauri#9524), so TanStack keeps every refetchInterval firing
// while the app idles in the tray (#2132). Pause queries when the backend
// hides the window; on focus, hand control back to TanStack's own
// visibilitychange detection (setFocused(undefined)) so platforms where it
// works, like macOS minimize, keep pausing natively.
function createHiddenWindowQueryPause(currentWindow: WebviewWindow) {
if (currentWindow.label !== "main") return;

let focusGeneration = 0;

const unlisteners = [
currentWindow.listen("main-window-hidden", () => {
focusManager.setFocused(false);
}),
currentWindow.onFocusChanged((event) => {
focusGeneration += 1;
if (event.payload) {
focusManager.setFocused(undefined);
return;
}
// Safety net for hide paths that bypass hide_main_window and
// hideCurrentWindow: a blur with the window no longer visible
// means hidden, not just unfocused. Not sufficient alone — an
// earlier benign blur (e.g. shell.open) masks a later hide. The
// generation guard stops a stale visibility result from pausing a
// window that regained focus while the check was in flight.
const generation = focusGeneration;
void currentWindow.isVisible().then((visible) => {
if (visible || generation !== focusGeneration) return;
focusManager.setFocused(false);
});
}),
];

onCleanup(() => {
for (const unlisten of unlisteners) void unlisten.then((fn) => fn());
});
}

function createThemeListener(currentWindow: WebviewWindow) {
const [appTheme, setAppTheme] = createSignal<AppTheme | null | undefined>();
let disposed = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { makePersisted } from "@solid-primitives/storage";
import { getVersion } from "@tauri-apps/api/app";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { createEffect, createResource } from "solid-js";
import { createStore } from "solid-js/store";
import Tooltip from "~/components/Tooltip";
import { hideCurrentWindow } from "~/utils/hide-window";
import { commands } from "~/utils/tauri";
import { apiClient } from "~/utils/web-api";
import IconLucideBell from "~icons/lucide/bell";
Expand Down Expand Up @@ -36,7 +36,7 @@ const ChangelogButton = () => {

const handleChangelogClick = () => {
commands.showWindow({ Settings: { page: "changelog" } });
getCurrentWindow().hide();
hideCurrentWindow();
const version = currentVersion();
if (version) {
setChangelogState({
Expand Down
11 changes: 6 additions & 5 deletions apps/desktop/src/routes/(window-chrome)/new-main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
type MicrophoneWithDetails,
} from "~/utils/devices";
import { clientEnv } from "~/utils/env";
import { hideCurrentWindow } from "~/utils/hide-window";
import {
importImageFromPicker,
importVideoFromPicker,
Expand Down Expand Up @@ -2024,7 +2025,7 @@ function Page() {
if (pickerActive && !hasHidden && !recording) {
setHasHiddenMainWindowForPicker(true);
setShouldRevealMainWindowAfterPicker(!editorPicker);
void getCurrentWindow().hide();
void hideCurrentWindow();
} else if (pickerActive && hasHidden) {
setShouldRevealMainWindowAfterPicker(!editorPicker);
} else if (recording) {
Expand Down Expand Up @@ -2922,7 +2923,7 @@ function Page() {
await shell.open(link);
}

await getCurrentWindow().hide();
await hideCurrentWindow();
};

const openScreenshot = async (screenshot: ScreenshotWithPath) => {
Expand Down Expand Up @@ -3224,7 +3225,7 @@ function Page() {
type="button"
onClick={async () => {
await commands.showWindow({ Settings: { page: "general" } });
getCurrentWindow().hide();
hideCurrentWindow();
}}
class="flex items-center justify-center size-5 focus:outline-hidden"
>
Expand Down Expand Up @@ -3412,7 +3413,7 @@ function Page() {
await commands.showWindow({
Settings: { page: "recordings" },
});
getCurrentWindow().hide();
hideCurrentWindow();
}}
uploadProgress={uploadProgress}
reuploadingPaths={reuploadingPaths()}
Expand All @@ -3436,7 +3437,7 @@ function Page() {
await commands.showWindow({
Settings: { page: "screenshots" },
});
getCurrentWindow().hide();
hideCurrentWindow();
}}
/>
) : variant === "camera" ? (
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/utils/hide-window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { focusManager } from "@tanstack/solid-query";
import { getCurrentWindow } from "@tauri-apps/api/window";

// Counterpart of the Rust-side hide_main_window for hides initiated by the
// window's own frontend. An earlier blur (e.g. shell.open stealing focus)
// leaves a later hide invisible to the focus bridge, and
// document.visibilityState never flips on Windows (tauri-apps/tauri#9524),
// so the polling pause must be explicit — after the hide succeeds, so a
// failed hide never pauses a still-visible window.
export async function hideCurrentWindow() {
const currentWindow = getCurrentWindow();
await currentWindow.hide();
if (currentWindow.label === "main") focusManager.setFocused(false);
}
4 changes: 2 additions & 2 deletions apps/desktop/src/utils/importMedia.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import * as dialog from "@tauri-apps/plugin-dialog";
import { hideCurrentWindow } from "~/utils/hide-window";
import { commands } from "~/utils/tauri";

const videoExtensions = [
Expand Down Expand Up @@ -32,7 +32,7 @@ const selectedPath = (result: string | string[] | null) =>
typeof result === "string" ? result : null;

const maybeHideCurrentWindow = async (options?: ImportOptions) => {
if (options?.hideCurrentWindow) await getCurrentWindow().hide();
if (options?.hideCurrentWindow) await hideCurrentWindow();
};

export const importVideoPath = async (
Expand Down
9 changes: 7 additions & 2 deletions crates/camera-directshow/examples/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ mod windows {

let device = selected.0;

let video_control = device.output_pin().cast::<IAMVideoControl>().ok();
let output_pin = device
.output_pin()
.expect("failed to bind capture filter for selected device")
.clone();

let video_control = output_pin.cast::<IAMVideoControl>().ok();

let formats = device
.media_types()
Expand All @@ -65,7 +70,7 @@ mod windows {

if let Some(video_control) = &video_control {
let time_per_frame_list = video_control.time_per_frame_list(
device.output_pin(),
&output_pin,
i as i32,
SIZE {
cx: width,
Expand Down
Loading
Loading