From ee5178849e9767545e52ceccedf3836a42be2fe5 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 18:36:09 +0800 Subject: [PATCH 01/12] wipwip --- .../routes/(window-chrome)/new-main/index.tsx | 140 ++++++++---------- .../new-main/useSystemHardwareOptions.ts | 106 +++++++++++++ 2 files changed, 164 insertions(+), 82 deletions(-) create mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index de21995239..57b090f299 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -71,6 +71,7 @@ import SystemAudio from "./SystemAudio"; import TargetDropdownButton from "./TargetDropdownButton"; import TargetMenuGrid from "./TargetMenuGrid"; import TargetTypeButton from "./TargetTypeButton"; +import { useSystemHardwareOptions } from "./useSystemHardwareOptions"; function getWindowSize() { return { @@ -79,15 +80,6 @@ function getWindowSize() { }; } -const findCamera = (cameras: CameraInfo[], id: DeviceOrModelID) => { - return cameras.find((c) => { - if (!id) return false; - return "DeviceID" in id - ? id.DeviceID === c.device_id - : id.ModelID === c.model_id; - }); -}; - type WindowListItem = Pick< CaptureWindow, "id" | "owner_name" | "name" | "bounds" | "refresh_rate" @@ -326,9 +318,8 @@ function Page() { refetchInterval: false, })); - const screens = useQuery(() => listScreens); - const windows = useQuery(() => listWindows); - + const { screens, windows, cameras, mics, options } = + useSystemHardwareOptions(); const hasDisplayTargetsData = () => displayTargets.status === "success"; const hasWindowTargetsData = () => windowTargets.status === "success"; @@ -449,9 +440,6 @@ function Page() { if (!monitor) return; }); - const cameras = useQuery(() => listVideoDevices); - const mics = useQuery(() => listAudioDevices); - const windowListSignature = createMemo(() => createWindowSignature(windows.data), ); @@ -494,73 +482,61 @@ function Page() { void displayTargets.refetch(); }); - cameras.promise.then((cameras) => { - if (rawOptions.cameraID && findCamera(cameras, rawOptions.cameraID)) { - setOptions("cameraLabel", null); - } - }); - - mics.promise.then((mics) => { - if (rawOptions.micName && !mics.includes(rawOptions.micName)) { - setOptions("micName", null); - } - }); - - const options = { - screen: () => { - let screen; - - if (rawOptions.captureTarget.variant === "display") { - const screenId = rawOptions.captureTarget.id; - screen = - screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; - } else if (rawOptions.captureTarget.variant === "area") { - const screenId = rawOptions.captureTarget.screen; - screen = - screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; - } - - return screen; - }, - window: () => { - let win; - - if (rawOptions.captureTarget.variant === "window") { - const windowId = rawOptions.captureTarget.id; - win = windows.data?.find((s) => s.id === windowId) ?? windows.data?.[0]; - } - - return win; - }, - camera: () => { - if (!rawOptions.cameraID) return undefined; - return findCamera(cameras.data || [], rawOptions.cameraID); - }, - micName: () => mics.data?.find((name) => name === rawOptions.micName), - target: (): ScreenCaptureTarget | undefined => { - switch (rawOptions.captureTarget.variant) { - case "display": { - const screen = options.screen(); - if (!screen) return; - return { variant: "display", id: screen.id }; - } - case "window": { - const window = options.window(); - if (!window) return; - return { variant: "window", id: window.id }; - } - case "area": { - const screen = options.screen(); - if (!screen) return; - return { - variant: "area", - bounds: rawOptions.captureTarget.bounds, - screen: screen.id, - }; - } - } - }, - }; + // const options = { + // screen: () => { + // let screen; + + // if (rawOptions.captureTarget.variant === "display") { + // const screenId = rawOptions.captureTarget.id; + // screen = + // screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; + // } else if (rawOptions.captureTarget.variant === "area") { + // const screenId = rawOptions.captureTarget.screen; + // screen = + // screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; + // } + + // return screen; + // }, + // window: () => { + // let win; + + // if (rawOptions.captureTarget.variant === "window") { + // const windowId = rawOptions.captureTarget.id; + // win = windows.data?.find((s) => s.id === windowId) ?? windows.data?.[0]; + // } + + // return win; + // }, + // camera: () => { + // if (!rawOptions.cameraID) return undefined; + // return findCamera(cameras.data || [], rawOptions.cameraID); + // }, + // micName: () => mics.data?.find((name) => name === rawOptions.micName), + // target: (): ScreenCaptureTarget | undefined => { + // switch (rawOptions.captureTarget.variant) { + // case "display": { + // const screen = options.screen(); + // if (!screen) return; + // return { variant: "display", id: screen.id }; + // } + // case "window": { + // const window = options.window(); + // if (!window) return; + // return { variant: "window", id: window.id }; + // } + // case "area": { + // const screen = options.screen(); + // if (!screen) return; + // return { + // variant: "area", + // bounds: rawOptions.captureTarget.bounds, + // screen: screen.id, + // }; + // } + // } + // }, + // }; createEffect(() => { const target = options.target(); diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts b/apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts new file mode 100644 index 0000000000..49f8b2cf4e --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts @@ -0,0 +1,106 @@ +import { useQuery } from "@tanstack/solid-query"; +import { + listAudioDevices, + listScreens, + listVideoDevices, + listWindows, +} from "~/utils/queries"; +import { useRecordingOptions } from "../OptionsContext"; +import type { + CameraInfo, + DeviceOrModelID, + ScreenCaptureTarget, +} from "~/utils/tauri"; + +const findCamera = (cameras: CameraInfo[], id: DeviceOrModelID) => { + return cameras.find((c) => { + if (!id) return false; + return "DeviceID" in id + ? id.DeviceID === c.device_id + : id.ModelID === c.model_id; + }); +}; + +export function useSystemHardwareOptions() { + const { rawOptions, setOptions } = useRecordingOptions(); + const screens = useQuery(() => listScreens); + const windows = useQuery(() => listWindows); + const cameras = useQuery(() => listVideoDevices); + const mics = useQuery(() => listAudioDevices); + + cameras.promise.then((cameras) => { + if (rawOptions.cameraID && findCamera(cameras, rawOptions.cameraID)) { + setOptions("cameraLabel", null); + } + }); + + mics.promise.then((mics) => { + if (rawOptions.micName && !mics.includes(rawOptions.micName)) { + setOptions("micName", null); + } + }); + + const options = { + screen: () => { + let screen; + + if (rawOptions.captureTarget.variant === "display") { + const screenId = rawOptions.captureTarget.id; + screen = + screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; + } else if (rawOptions.captureTarget.variant === "area") { + const screenId = rawOptions.captureTarget.screen; + screen = + screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; + } + + return screen; + }, + window: () => { + let win; + + if (rawOptions.captureTarget.variant === "window") { + const windowId = rawOptions.captureTarget.id; + win = windows.data?.find((s) => s.id === windowId) ?? windows.data?.[0]; + } + + return win; + }, + camera: () => { + if (!rawOptions.cameraID) return undefined; + return findCamera(cameras.data || [], rawOptions.cameraID); + }, + micName: () => mics.data?.find((name) => name === rawOptions.micName), + target: (): ScreenCaptureTarget | undefined => { + switch (rawOptions.captureTarget.variant) { + case "display": { + const screen = options.screen(); + if (!screen) return; + return { variant: "display", id: screen.id }; + } + case "window": { + const window = options.window(); + if (!window) return; + return { variant: "window", id: window.id }; + } + case "area": { + const screen = options.screen(); + if (!screen) return; + return { + variant: "area", + bounds: rawOptions.captureTarget.bounds, + screen: screen.id, + }; + } + } + }, + }; + + return { + screens, + windows, + cameras, + mics, + options, + }; +} From c16f2a338f4d5c5ce32bcb233710a8a0f1771a21 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 18:45:47 +0800 Subject: [PATCH 02/12] wip --- .../(window-chrome)/new-main/BaseControls.tsx | 54 +++++++++ .../routes/(window-chrome)/new-main/index.tsx | 112 +----------------- .../src/routes/target-select-overlay.tsx | 6 + 3 files changed, 62 insertions(+), 110 deletions(-) create mode 100644 apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx new file mode 100644 index 0000000000..11544ac146 --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx @@ -0,0 +1,54 @@ +import { onMount } from "solid-js"; +import CameraSelect from "./CameraSelect"; +import MicrophoneSelect from "./MicrophoneSelect"; +import SystemAudio from "./SystemAudio"; +import { useSystemHardwareOptions } from "./useSystemHardwareOptions"; +import { useRecordingOptions } from "../OptionsContext"; +import { createCameraMutation } from "~/utils/queries"; +import { createMutation } from "@tanstack/solid-query"; +import { commands } from "~/utils/tauri"; + +export function BaseControls() { + const { rawOptions, setOptions } = useRecordingOptions(); + const { cameras, mics, options } = useSystemHardwareOptions(); + + const setCamera = createCameraMutation(); + const setMicInput = createMutation(() => ({ + mutationFn: async (name: string | null) => { + await commands.setMicInput(name); + setOptions("micName", name); + }, + })); + + onMount(() => { + if (rawOptions.cameraID && "ModelID" in rawOptions.cameraID) + setCamera.mutate({ ModelID: rawOptions.cameraID.ModelID }); + else if (rawOptions.cameraID && "DeviceID" in rawOptions.cameraID) + setCamera.mutate({ DeviceID: rawOptions.cameraID.DeviceID }); + else setCamera.mutate(null); + }); + + return ( +
+ { + if (!c) setCamera.mutate(null); + else if (c.model_id) setCamera.mutate({ ModelID: c.model_id }); + else setCamera.mutate({ DeviceID: c.device_id }); + }} + /> + setMicInput.mutate(v)} + /> + +
+ ); +} diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index 57b090f299..4cdaef26c3 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui-solid"; import { createEventListener } from "@solid-primitives/event-listener"; import { useNavigate } from "@solidjs/router"; -import { createMutation, useQuery } from "@tanstack/solid-query"; +import { useQuery } from "@tanstack/solid-query"; import { listen } from "@tauri-apps/api/event"; import { getAllWebviewWindows, @@ -34,25 +34,17 @@ import { Input } from "~/routes/editor/ui"; import { generalSettingsStore } from "~/store"; import { createSignInMutation } from "~/utils/auth"; import { - createCameraMutation, createCurrentRecordingQuery, createLicenseQuery, - listAudioDevices, listDisplaysWithThumbnails, - listScreens, - listVideoDevices, - listWindows, listWindowsWithThumbnails, } from "~/utils/queries"; import { - type CameraInfo, type CaptureDisplay, type CaptureDisplayWithThumbnail, type CaptureWindow, type CaptureWindowWithThumbnail, commands, - type DeviceOrModelID, - type ScreenCaptureTarget, } from "~/utils/tauri"; import IconLucideAppWindowMac from "~icons/lucide/app-window-mac"; import IconLucideArrowLeft from "~icons/lucide/arrow-left"; @@ -64,14 +56,12 @@ import { RecordingOptionsProvider, useRecordingOptions, } from "../OptionsContext"; -import CameraSelect from "./CameraSelect"; import ChangelogButton from "./ChangeLogButton"; -import MicrophoneSelect from "./MicrophoneSelect"; -import SystemAudio from "./SystemAudio"; import TargetDropdownButton from "./TargetDropdownButton"; import TargetMenuGrid from "./TargetMenuGrid"; import TargetTypeButton from "./TargetTypeButton"; import { useSystemHardwareOptions } from "./useSystemHardwareOptions"; +import { BaseControls } from "./BaseControls"; function getWindowSize() { return { @@ -482,62 +472,6 @@ function Page() { void displayTargets.refetch(); }); - // const options = { - // screen: () => { - // let screen; - - // if (rawOptions.captureTarget.variant === "display") { - // const screenId = rawOptions.captureTarget.id; - // screen = - // screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; - // } else if (rawOptions.captureTarget.variant === "area") { - // const screenId = rawOptions.captureTarget.screen; - // screen = - // screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; - // } - - // return screen; - // }, - // window: () => { - // let win; - - // if (rawOptions.captureTarget.variant === "window") { - // const windowId = rawOptions.captureTarget.id; - // win = windows.data?.find((s) => s.id === windowId) ?? windows.data?.[0]; - // } - - // return win; - // }, - // camera: () => { - // if (!rawOptions.cameraID) return undefined; - // return findCamera(cameras.data || [], rawOptions.cameraID); - // }, - // micName: () => mics.data?.find((name) => name === rawOptions.micName), - // target: (): ScreenCaptureTarget | undefined => { - // switch (rawOptions.captureTarget.variant) { - // case "display": { - // const screen = options.screen(); - // if (!screen) return; - // return { variant: "display", id: screen.id }; - // } - // case "window": { - // const window = options.window(); - // if (!window) return; - // return { variant: "window", id: window.id }; - // } - // case "area": { - // const screen = options.screen(); - // if (!screen) return; - // return { - // variant: "area", - // bounds: rawOptions.captureTarget.bounds, - // screen: screen.id, - // }; - // } - // } - // }, - // }; - createEffect(() => { const target = options.target(); if (!target) return; @@ -552,51 +486,9 @@ function Page() { } }); - const setMicInput = createMutation(() => ({ - mutationFn: async (name: string | null) => { - await commands.setMicInput(name); - setOptions("micName", name); - }, - })); - - const setCamera = createCameraMutation(); - - onMount(() => { - if (rawOptions.cameraID && "ModelID" in rawOptions.cameraID) - setCamera.mutate({ ModelID: rawOptions.cameraID.ModelID }); - else if (rawOptions.cameraID && "DeviceID" in rawOptions.cameraID) - setCamera.mutate({ DeviceID: rawOptions.cameraID.DeviceID }); - else setCamera.mutate(null); - }); - const license = createLicenseQuery(); - const signIn = createSignInMutation(); - const BaseControls = () => ( -
- { - if (!c) setCamera.mutate(null); - else if (c.model_id) setCamera.mutate({ ModelID: c.model_id }); - else setCamera.mutate({ DeviceID: c.device_id }); - }} - /> - setMicInput.mutate(v)} - /> - -
- ); - const TargetSelectionHome = () => ( + +
+ +
+
props.setToggleModeSelect?.(true)} class="flex gap-1 items-center mb-5 transition-opacity duration-200 hover:opacity-60" From 521b41121e6e9e8e3756e6cb13682d2c37cc60f7 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 18:47:02 +0800 Subject: [PATCH 03/12] v2 --- .../src/routes/(window-chrome)/new-main/BaseControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx index 11544ac146..78cdec1d77 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx @@ -29,7 +29,7 @@ export function BaseControls() { }); return ( -
+
Date: Thu, 23 Oct 2025 18:50:16 +0800 Subject: [PATCH 04/12] wip --- .../src/routes/target-select-overlay.tsx | 126 +++++++++--------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index 6802f0397b..2323db095c 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -875,82 +875,86 @@ function RecordingControls(props: {
} > -
-
{ - setOptions("targetMode", null); - commands.closeTargetSelectOverlays(); - }} - class="flex justify-center items-center rounded-full transition-opacity bg-gray-12 size-9 hover:opacity-80" - > - -
-
{ - if (rawOptions.mode === "instant" && !auth.data) { - emit("start-sign-in"); - return; - } - if (startRecording.isPending) return; - - startRecording.mutate(); - }} - > +
+
+
{ + setOptions("targetMode", null); + commands.closeTargetSelectOverlays(); + }} + class="flex justify-center items-center rounded-full transition-opacity bg-gray-12 size-9 hover:opacity-80" + > + +
{ + if (rawOptions.mode === "instant" && !auth.data) { + emit("start-sign-in"); + return; + } + if (startRecording.isPending) return; + + startRecording.mutate(); + }} > - {rawOptions.mode === "studio" ? ( - - ) : ( - - )} -
- - {rawOptions.mode === "instant" && !auth.data - ? "Sign In To Use" - : "Start Recording"} - - - {`${capitalize(rawOptions.mode)} Mode`} - +
+ {rawOptions.mode === "studio" ? ( + + ) : ( + + )} +
+ + {rawOptions.mode === "instant" && !auth.data + ? "Sign In To Use" + : "Start Recording"} + + + {`${capitalize(rawOptions.mode)} Mode`} + +
+
+
{ + e.stopPropagation(); + menuModes().then((menu) => menu.popup()); + }} + > +
{ e.stopPropagation(); - menuModes().then((menu) => menu.popup()); + preRecordingMenu().then((menu) => menu.popup()); }} + class="flex justify-center items-center rounded-full border transition-opacity bg-gray-6 text-gray-12 size-9 hover:opacity-80" > - +
-
{ - e.stopPropagation(); - preRecordingMenu().then((menu) => menu.popup()); - }} - class="flex justify-center items-center rounded-full border transition-opacity bg-gray-6 text-gray-12 size-9 hover:opacity-80" - > - -
-
-
+ {/*
+ +
*/} +
props.setToggleModeSelect?.(true)} class="flex gap-1 items-center mb-5 transition-opacity duration-200 hover:opacity-60" From fe7b81d16c658ed9cff2842c0bf6e38567295d3a Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 18:53:27 +0800 Subject: [PATCH 05/12] wip --- .../src/routes/(window-chrome)/new-main/BaseControls.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx index 78cdec1d77..b0be0bd810 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx @@ -29,7 +29,7 @@ export function BaseControls() { }); return ( -
+
setMicInput.mutate(v)} /> - + {/**/}
); } From 51674dd684de6caeb989310659c2fd7607510e93 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 19:02:05 +0800 Subject: [PATCH 06/12] wip --- .../src/routes/(window-chrome)/new-main/BaseControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx index b0be0bd810..19e980ad32 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx @@ -29,7 +29,7 @@ export function BaseControls() { }); return ( -
+
Date: Thu, 23 Oct 2025 19:54:33 +0800 Subject: [PATCH 07/12] wip --- apps/desktop/src-tauri/src/lib.rs | 4 ++ .../src-tauri/src/target_select_overlay.rs | 47 +++++++++++++++---- .../src/routes/target-select-overlay.tsx | 6 --- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 2a0eb78225..1e75a06674 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2184,6 +2184,10 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { #[cfg(target_os = "macos")] app.manage(crate::platform::ScreenCapturePrewarmer::default()); + tokio::spawn(async move { + target_select_overlay::init(&app).await; + }); + tokio::spawn({ let camera_feed = camera_feed.clone(); let app = app.clone(); diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index 31a030d6b7..b4a1561ba6 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -7,6 +7,7 @@ use std::{ use base64::prelude::*; use cap_recording::screen_capture::ScreenCaptureTarget; +use futures::future::join_all; use crate::windows::{CapWindowId, ShowCapWindow}; use scap_targets::{ @@ -41,6 +42,28 @@ pub struct DisplayInformation { refresh_rate: String, } +// We create the windows hidden at app launch so they are ready when used. +// Otherwise we have noticed they can take a while to load on first interaction (especially on Windows). +pub async fn init(app: &AppHandle) { + join_all( + scap_targets::Display::list() + .into_iter() + .map(|d| d.id()) + .map(|display_id| { + let app = app.clone(); + + async move { + ShowCapWindow::TargetSelectOverlay { display_id } + .show(&app) + .await + .map_err(|err| error!("Error initializing target select overlay: {err}")) + .ok(); + } + }), + ) + .await; +} + #[specta::specta] #[tauri::command] #[instrument(skip(app, state))] @@ -49,15 +72,21 @@ pub async fn open_target_select_overlays( state: tauri::State<'_, WindowFocusManager>, focused_target: Option, ) -> Result<(), String> { - let displays = scap_targets::Display::list() - .into_iter() - .map(|d| d.id()) - .collect::>(); - for display_id in displays { - let _ = ShowCapWindow::TargetSelectOverlay { display_id } - .show(&app) - .await; - } + join_all( + scap_targets::Display::list() + .into_iter() + .map(|d| d.id()) + .map(|display_id| { + let app = app.clone(); + + async move { + ShowCapWindow::TargetSelectOverlay { display_id } + .show(&app) + .await + } + }), + ) + .await; let handle = tokio::spawn({ let app = app.clone(); diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index cecab14f97..393de284a5 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -995,9 +995,3 @@ function ResizeHandle( /> ); } - -function getDisplayId(displayId: string | undefined) { - const id = Number(displayId); - if (Number.isNaN(id)) return 0; - return id; -} From 72402c2d10b2cf74b992227f867005b5af6d724c Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 19:55:58 +0800 Subject: [PATCH 08/12] clone app --- apps/desktop/src-tauri/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 1e75a06674..a8c948c7cc 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2184,8 +2184,11 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { #[cfg(target_os = "macos")] app.manage(crate::platform::ScreenCapturePrewarmer::default()); - tokio::spawn(async move { - target_select_overlay::init(&app).await; + tokio::spawn({ + let app = app.clone(); + async move { + target_select_overlay::init(&app).await; + } }); tokio::spawn({ From b545403a83da7a7cc40f2120092157b821d50414 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 20:20:09 +0800 Subject: [PATCH 09/12] hide instead of closing --- apps/desktop/src-tauri/src/lib.rs | 2 +- apps/desktop/src-tauri/src/recording.rs | 2 +- .../src-tauri/src/target_select_overlay.rs | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index a8c948c7cc..cbb8d1c828 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2358,7 +2358,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { if let Ok(CapWindowId::TargetSelectOverlay { .. }) = CapWindowId::from_str(&id) { - let _ = window.close(); + let _ = window.hide(); } } diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 10b136e552..5e4ae9236f 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -415,7 +415,7 @@ pub async fn start_recording( .filter_map(|(label, win)| CapWindowId::from_str(label).ok().map(|id| (id, win))) { if matches!(id, CapWindowId::TargetSelectOverlay { .. }) { - win.close().ok(); + win.hide().ok(); } } let _ = ShowCapWindow::InProgressRecording { countdown } diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index b4a1561ba6..c0679ccb1e 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -53,11 +53,14 @@ pub async fn init(app: &AppHandle) { let app = app.clone(); async move { - ShowCapWindow::TargetSelectOverlay { display_id } + let result = ShowCapWindow::TargetSelectOverlay { display_id } .show(&app) .await - .map_err(|err| error!("Error initializing target select overlay: {err}")) - .ok(); + .map_err(|err| error!("Error initializing target select overlay: {err}")); + + if let Ok(window) = result { + window.hide().ok(); + } } }), ) @@ -83,6 +86,8 @@ pub async fn open_target_select_overlays( ShowCapWindow::TargetSelectOverlay { display_id } .show(&app) .await + .map_err(|err| error!("Error initializing target select overlay: {err}")) + .ok(); } }), ) @@ -145,7 +150,10 @@ pub async fn open_target_select_overlays( pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> { for (id, window) in app.webview_windows() { if let Ok(CapWindowId::TargetSelectOverlay { .. }) = CapWindowId::from_str(&id) { - let _ = window.close(); + window + .hide() + .map_err(|err| error!("Error hiding target select overlay: {err}")) + .ok(); } } From 9bd864e53b2df481cf227c7906e4224cf742bf88 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 20:41:34 +0800 Subject: [PATCH 10/12] wip --- apps/desktop/src-tauri/src/lib.rs | 6 +- .../src-tauri/src/target_select_overlay.rs | 114 ++++-------------- apps/desktop/src-tauri/src/windows.rs | 9 -- 3 files changed, 26 insertions(+), 103 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index cbb8d1c828..28045bbbbc 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2179,7 +2179,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { hotkeys::init(&app); general_settings::init(&app); fake_window::init(&app); - app.manage(target_select_overlay::WindowFocusManager::default()); + app.manage(target_select_overlay::State::default()); app.manage(EditorWindowIds::default()); #[cfg(target_os = "macos")] app.manage(crate::platform::ScreenCapturePrewarmer::default()); @@ -2423,10 +2423,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { } return; } - CapWindowId::TargetSelectOverlay { display_id } => { - app.state::() - .destroy(&display_id, app.global_shortcut()); - } CapWindowId::Camera => { let app = app.clone(); tokio::spawn(async move { diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index c0679ccb1e..4d4b1f5e4e 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -1,5 +1,4 @@ use std::{ - collections::HashMap, str::FromStr, sync::{Mutex, PoisonError}, time::Duration, @@ -16,8 +15,8 @@ use scap_targets::{ }; use serde::Serialize; use specta::Type; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tauri_plugin_global_shortcut::{GlobalShortcut, GlobalShortcutExt}; +use tauri::{AppHandle, Manager}; +use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_specta::Event; use tokio::task::JoinHandle; use tracing::{error, instrument}; @@ -67,12 +66,15 @@ pub async fn init(app: &AppHandle) { .await; } +#[derive(Default)] +pub struct State(Mutex>>); + #[specta::specta] #[tauri::command] #[instrument(skip(app, state))] pub async fn open_target_select_overlays( app: AppHandle, - state: tauri::State<'_, WindowFocusManager>, + state: tauri::State<'_, State>, focused_target: Option, ) -> Result<(), String> { join_all( @@ -127,7 +129,7 @@ pub async fn open_target_select_overlays( }); if let Some(task) = state - .task + .0 .lock() .unwrap_or_else(PoisonError::into_inner) .replace(handle) @@ -146,8 +148,11 @@ pub async fn open_target_select_overlays( #[specta::specta] #[tauri::command] -#[instrument(skip(app))] -pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> { +#[instrument(skip(app, state))] +pub async fn close_target_select_overlays( + app: AppHandle, + state: tauri::State<'_, State>, +) -> Result<(), String> { for (id, window) in app.webview_windows() { if let Ok(CapWindowId::TargetSelectOverlay { .. }) = CapWindowId::from_str(&id) { window @@ -157,6 +162,19 @@ pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> } } + if let Some(task) = state + .0 + .lock() + .unwrap_or_else(PoisonError::into_inner) + .take() + { + task.abort(); + app.global_shortcut() + .unregister("Escape") + .map_err(|err| error!("Error unregistering global keyboard shortcut for Escape: {err}")) + .ok(); + } + Ok(()) } @@ -247,85 +265,3 @@ pub async fn focus_window(window_id: WindowId) -> Result<(), String> { Ok(()) } - -// Windows doesn't have a proper concept of window z-index's so we implement them in userspace :( -#[derive(Default)] -pub struct WindowFocusManager { - task: Mutex>>, - tasks: Mutex>>, -} - -impl WindowFocusManager { - /// Called when a window is created to spawn it's task - pub fn spawn(&self, id: &DisplayId, window: WebviewWindow) { - let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); - tasks.insert( - id.to_string(), - tokio::spawn(async move { - let app = window.app_handle(); - loop { - let cap_main = CapWindowId::Main.get(app); - let cap_settings = CapWindowId::Settings.get(app); - - let has_cap_main = cap_main - .as_ref() - .and_then(|v| Some(v.is_minimized().ok()? || !v.is_visible().ok()?)) - .unwrap_or(true); - let has_cap_settings = cap_settings - .and_then(|v| Some(v.is_minimized().ok()? || !v.is_visible().ok()?)) - .unwrap_or(true); - - // Close the overlay if the cap main and settings are not available. - if has_cap_main && has_cap_settings { - window.hide().ok(); - break; - } - - #[cfg(windows)] - if let Some(cap_main) = cap_main { - let should_refocus = cap_main.is_focused().ok().unwrap_or_default() - || window.is_focused().unwrap_or_default(); - - // If a Cap window is not focused we know something is trying to steal the focus. - // We need to move the overlay above it. We don't use `always_on_top` on the overlay because we need the Cap window to stay above it. - if !should_refocus { - window.set_focus().ok(); - } - } - - tokio::time::sleep(std::time::Duration::from_millis(400)).await; - } - }), - ); - } - - /// Called when a specific overlay window is destroyed to cleanup it's resources - pub fn destroy(&self, id: &DisplayId, global_shortcut: &GlobalShortcut) { - let mut tasks = self.tasks.lock().unwrap_or_else(PoisonError::into_inner); - if let Some(task) = tasks.remove(&id.to_string()) { - task.abort(); - } - - // When all overlay windows are closed cleanup shared resources. - if tasks.is_empty() { - // Unregister keyboard shortcut - // This messes with other applications if we don't remove it. - global_shortcut - .unregister("Escape") - .map_err(|err| { - error!("Error unregistering global keyboard shortcut for Escape: {err}") - }) - .ok(); - - // Shutdown the cursor tracking task - if let Some(task) = self - .task - .lock() - .unwrap_or_else(PoisonError::into_inner) - .take() - { - task.abort(); - } - } - } -} diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 47828293ae..9467c77377 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -25,7 +25,6 @@ use crate::{ general_settings::{self, AppTheme, GeneralSettingsStore}, permissions, recording_settings::RecordingTargetMode, - target_select_overlay::WindowFocusManager, window_exclusion::WindowExclusion, }; @@ -366,14 +365,6 @@ impl ShowCapWindow { } } - app.state::() - .spawn(display_id, window.clone()); - - #[cfg(target_os = "macos")] - { - crate::platform::set_window_level(window.as_ref().window(), 45); - } - window } Self::Settings { page } => { From 3ea52244755f48bc2f57f5c4f41ed75959546caa Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 20:56:13 +0800 Subject: [PATCH 11/12] fix tso spawning --- apps/desktop/src-tauri/src/target_select_overlay.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index 4d4b1f5e4e..2771ca3c33 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -77,7 +77,7 @@ pub async fn open_target_select_overlays( state: tauri::State<'_, State>, focused_target: Option, ) -> Result<(), String> { - join_all( + let windows = join_all( scap_targets::Display::list() .into_iter() .map(|d| d.id()) @@ -89,12 +89,21 @@ pub async fn open_target_select_overlays( .show(&app) .await .map_err(|err| error!("Error initializing target select overlay: {err}")) - .ok(); + .ok() } }), ) .await; + for window in windows { + if let Some(window) = window { + window + .show() + .map_err(|err| error!("Error showing target select overlay: {err}")) + .ok(); + } + } + let handle = tokio::spawn({ let app = app.clone(); From 5bae9ebff9952a902bbf75f7bbc51223c5b77d16 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Oct 2025 20:57:59 +0800 Subject: [PATCH 12/12] format --- .../src/routes/(window-chrome)/new-main/BaseControls.tsx | 8 ++++---- .../desktop/src/routes/(window-chrome)/new-main/index.tsx | 2 +- .../(window-chrome)/new-main/useSystemHardwareOptions.ts | 2 +- apps/desktop/src/routes/target-select-overlay.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx index 19e980ad32..7bb55c1938 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/BaseControls.tsx @@ -1,12 +1,12 @@ +import { createMutation } from "@tanstack/solid-query"; import { onMount } from "solid-js"; +import { createCameraMutation } from "~/utils/queries"; +import { commands } from "~/utils/tauri"; +import { useRecordingOptions } from "../OptionsContext"; import CameraSelect from "./CameraSelect"; import MicrophoneSelect from "./MicrophoneSelect"; import SystemAudio from "./SystemAudio"; import { useSystemHardwareOptions } from "./useSystemHardwareOptions"; -import { useRecordingOptions } from "../OptionsContext"; -import { createCameraMutation } from "~/utils/queries"; -import { createMutation } from "@tanstack/solid-query"; -import { commands } from "~/utils/tauri"; export function BaseControls() { const { rawOptions, setOptions } = useRecordingOptions(); diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index 4cdaef26c3..2c2cfd23cf 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -56,12 +56,12 @@ import { RecordingOptionsProvider, useRecordingOptions, } from "../OptionsContext"; +import { BaseControls } from "./BaseControls"; import ChangelogButton from "./ChangeLogButton"; import TargetDropdownButton from "./TargetDropdownButton"; import TargetMenuGrid from "./TargetMenuGrid"; import TargetTypeButton from "./TargetTypeButton"; import { useSystemHardwareOptions } from "./useSystemHardwareOptions"; -import { BaseControls } from "./BaseControls"; function getWindowSize() { return { diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts b/apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts index 49f8b2cf4e..428e183dd3 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts +++ b/apps/desktop/src/routes/(window-chrome)/new-main/useSystemHardwareOptions.ts @@ -5,12 +5,12 @@ import { listVideoDevices, listWindows, } from "~/utils/queries"; -import { useRecordingOptions } from "../OptionsContext"; import type { CameraInfo, DeviceOrModelID, ScreenCaptureTarget, } from "~/utils/tauri"; +import { useRecordingOptions } from "../OptionsContext"; const findCamera = (cameras: CameraInfo[], id: DeviceOrModelID) => { return cameras.find((c) => { diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index 41857cd371..5062d05c65 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -35,11 +35,11 @@ import { type ScreenCaptureTarget, type TargetUnderCursor, } from "~/utils/tauri"; +import { BaseControls } from "./(window-chrome)/new-main/BaseControls"; import { RecordingOptionsProvider, useRecordingOptions, } from "./(window-chrome)/OptionsContext"; -import { BaseControls } from "./(window-chrome)/new-main/BaseControls"; const MIN_WIDTH = 200; const MIN_HEIGHT = 100;