Skip to content

Commit 778d80f

Browse files
committed
feat: Basic window excluding UI
1 parent 7369e1f commit 778d80f

File tree

16 files changed

+695
-60
lines changed

16 files changed

+695
-60
lines changed

.claude/settings.local.json

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
2-
"permissions": {
3-
"allow": [
4-
"Bash(pnpm typecheck:*)",
5-
"Bash(pnpm lint:*)",
6-
"Bash(pnpm build:*)"
7-
],
8-
"deny": [],
9-
"ask": []
10-
}
11-
}
2+
"permissions": {
3+
"allow": [
4+
"Bash(pnpm typecheck:*)",
5+
"Bash(pnpm lint:*)",
6+
"Bash(pnpm build:*)",
7+
"Bash(cargo check:*)"
8+
],
9+
"deny": [],
10+
"ask": []
11+
}
12+
}

apps/desktop/src-tauri/src/general_settings.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use cap_recording::sources::screen_capture::WindowExclusion;
12
use serde::{Deserialize, Serialize};
23
use serde_json::json;
34
use specta::Type;
@@ -39,6 +40,30 @@ impl MainWindowRecordingStartBehaviour {
3940
}
4041
}
4142

43+
const DEFAULT_EXCLUDED_WINDOW_TITLES: &[&str] = &[
44+
"Cap",
45+
"Cap Setup",
46+
"Cap Settings",
47+
"Cap Editor",
48+
"Cap Mode Selection",
49+
"Cap Camera",
50+
"Cap Recordings Overlay",
51+
"Cap In Progress Recording",
52+
"Cap Window Capture Occluder",
53+
"Cap Capture Area",
54+
];
55+
56+
pub fn default_excluded_windows() -> Vec<WindowExclusion> {
57+
DEFAULT_EXCLUDED_WINDOW_TITLES
58+
.iter()
59+
.map(|title| WindowExclusion {
60+
bundle_identifier: None,
61+
owner_name: None,
62+
window_title: Some((*title).to_string()),
63+
})
64+
.collect()
65+
}
66+
4267
// When adding fields here, #[serde(default)] defines the value to use for existing configurations,
4368
// and `Default::default` defines the value to use for new configurations.
4469
// Things that affect the user experience should only be enabled by default for new configurations.
@@ -99,6 +124,8 @@ pub struct GeneralSettingsStore {
99124
pub post_deletion_behaviour: PostDeletionBehaviour,
100125
#[serde(default = "default_enable_new_uploader", skip_serializing_if = "no")]
101126
pub enable_new_uploader: bool,
127+
#[serde(default = "default_excluded_windows")]
128+
pub excluded_windows: Vec<WindowExclusion>,
102129
}
103130

104131
fn default_enable_native_camera_preview() -> bool {
@@ -162,6 +189,7 @@ impl Default for GeneralSettingsStore {
162189
enable_new_recording_flow: default_enable_new_recording_flow(),
163190
post_deletion_behaviour: PostDeletionBehaviour::DoNothing,
164191
enable_new_uploader: default_enable_new_uploader(),
192+
excluded_windows: default_excluded_windows(),
165193
}
166194
}
167195
}
@@ -213,6 +241,17 @@ impl GeneralSettingsStore {
213241
store.set("general_settings", json!(self));
214242
store.save().map_err(|e| e.to_string())
215243
}
244+
245+
pub fn is_window_excluded(
246+
&self,
247+
bundle_identifier: Option<&str>,
248+
owner_name: Option<&str>,
249+
window_title: Option<&str>,
250+
) -> bool {
251+
self.excluded_windows
252+
.iter()
253+
.any(|entry| entry.matches(bundle_identifier, owner_name, window_title))
254+
}
216255
}
217256

218257
pub fn init(app: &AppHandle) {
@@ -231,3 +270,9 @@ pub fn init(app: &AppHandle) {
231270

232271
println!("GeneralSettingsState managed");
233272
}
273+
274+
#[tauri::command]
275+
#[specta::specta]
276+
pub fn get_default_excluded_windows() -> Vec<WindowExclusion> {
277+
default_excluded_windows()
278+
}

apps/desktop/src-tauri/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1914,6 +1914,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
19141914
recording::list_capture_displays,
19151915
recording::list_displays_with_thumbnails,
19161916
recording::list_windows_with_thumbnails,
1917+
windows::refresh_window_content_protection,
1918+
general_settings::get_default_excluded_windows,
19171919
take_screenshot,
19181920
list_audio_devices,
19191921
close_recordings_overlay_window,
@@ -2014,7 +2016,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
20142016
.typ::<hotkeys::HotkeysStore>()
20152017
.typ::<general_settings::GeneralSettingsStore>()
20162018
.typ::<recording_settings::RecordingSettingsStore>()
2017-
.typ::<cap_flags::Flags>();
2019+
.typ::<cap_flags::Flags>()
2020+
.typ::<cap_recording::sources::screen_capture::WindowExclusion>();
20182021

20192022
#[cfg(debug_assertions)]
20202023
specta_builder

apps/desktop/src-tauri/src/recording.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ use crate::{
4444
audio::AppSounds,
4545
auth::AuthStore,
4646
create_screenshot,
47-
general_settings::{GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour},
47+
general_settings::{
48+
self, GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour,
49+
},
4850
open_external_link,
4951
presets::PresetsStore,
5052
thumbnails::*,
@@ -457,6 +459,11 @@ pub async fn start_recording(
457459
recording_dir: recording_dir.clone(),
458460
};
459461

462+
let excluded_windows = general_settings
463+
.as_ref()
464+
.map(|settings| settings.excluded_windows.clone())
465+
.unwrap_or_else(general_settings::default_excluded_windows);
466+
460467
let actor = match inputs.mode {
461468
RecordingMode::Studio => {
462469
let mut builder = studio_recording::Actor::builder(
@@ -468,7 +475,8 @@ pub async fn start_recording(
468475
general_settings
469476
.map(|s| s.custom_cursor_capture)
470477
.unwrap_or_default(),
471-
);
478+
)
479+
.with_excluded_windows(excluded_windows.clone());
472480

473481
if let Some(camera_feed) = camera_feed {
474482
builder = builder.with_camera_feed(camera_feed);
@@ -508,7 +516,8 @@ pub async fn start_recording(
508516
recording_dir.clone(),
509517
inputs.capture_target.clone(),
510518
)
511-
.with_system_audio(inputs.capture_system_audio);
519+
.with_system_audio(inputs.capture_system_audio)
520+
.with_excluded_windows(excluded_windows.clone());
512521

513522
if let Some(mic_feed) = mic_feed {
514523
builder = builder.with_mic_feed(mic_feed);

apps/desktop/src-tauri/src/thumbnails/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub struct CaptureWindowWithThumbnail {
3333
pub refresh_rate: u32,
3434
pub thumbnail: Option<String>,
3535
pub app_icon: Option<String>,
36+
pub bundle_identifier: Option<String>,
3637
}
3738

3839
pub fn normalize_thumbnail_dimensions(image: &image::RgbaImage) -> image::RgbaImage {
@@ -140,6 +141,7 @@ pub async fn collect_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithTh
140141
refresh_rate: capture_window.refresh_rate,
141142
thumbnail,
142143
app_icon,
144+
bundle_identifier: capture_window.bundle_identifier,
143145
});
144146
}
145147

apps/desktop/src-tauri/src/windows.rs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ use tracing::{debug, error, warn};
2222

2323
use crate::{
2424
App, ArcLock, RequestScreenCapturePrewarm, fake_window,
25-
general_settings::{AppTheme, GeneralSettingsStore},
25+
general_settings::{self, AppTheme, GeneralSettingsStore},
2626
permissions,
2727
recording_settings::RecordingTargetMode,
2828
target_select_overlay::WindowFocusManager,
2929
};
30+
use cap_recording::sources::screen_capture::WindowExclusion;
3031

3132
#[cfg(target_os = "macos")]
3233
const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition<f64> = LogicalPosition::new(12.0, 12.0);
@@ -250,6 +251,9 @@ impl ShowCapWindow {
250251
.map(|s| s.enable_new_recording_flow)
251252
.unwrap_or_default();
252253

254+
let title = CapWindowId::Main.title();
255+
let should_protect = should_protect_window(app, &title);
256+
253257
let window = self
254258
.window_builder(app, if new_recording_flow { "/new-main" } else { "/" })
255259
.resizable(false)
@@ -258,7 +262,7 @@ impl ShowCapWindow {
258262
.minimizable(false)
259263
.always_on_top(true)
260264
.visible_on_all_workspaces(true)
261-
.content_protected(false)
265+
.content_protected(should_protect)
262266
.center()
263267
.initialization_script(format!(
264268
"
@@ -296,6 +300,12 @@ impl ShowCapWindow {
296300
return Err(tauri::Error::WindowNotFound);
297301
};
298302

303+
let title = CapWindowId::TargetSelectOverlay {
304+
display_id: display_id.clone(),
305+
}
306+
.title();
307+
let should_protect = should_protect_window(app, &title);
308+
299309
let mut window_builder = self
300310
.window_builder(
301311
app,
@@ -305,7 +315,7 @@ impl ShowCapWindow {
305315
.resizable(false)
306316
.fullscreen(false)
307317
.shadow(false)
308-
.content_protected(true)
318+
.content_protected(should_protect)
309319
.always_on_top(true)
310320
.visible_on_all_workspaces(true)
311321
.skip_taskbar(true)
@@ -516,6 +526,12 @@ impl ShowCapWindow {
516526
return Err(tauri::Error::WindowNotFound);
517527
};
518528

529+
let title = CapWindowId::WindowCaptureOccluder {
530+
screen_id: screen_id.clone(),
531+
}
532+
.title();
533+
let should_protect = should_protect_window(app, &title);
534+
519535
#[cfg(target_os = "macos")]
520536
let position = display.raw_handle().logical_position();
521537

@@ -532,7 +548,7 @@ impl ShowCapWindow {
532548
.shadow(false)
533549
.always_on_top(true)
534550
.visible_on_all_workspaces(true)
535-
.content_protected(true)
551+
.content_protected(should_protect)
536552
.skip_taskbar(true)
537553
.inner_size(bounds.width(), bounds.height())
538554
.position(position.x(), position.y())
@@ -550,13 +566,16 @@ impl ShowCapWindow {
550566
window
551567
}
552568
Self::CaptureArea { screen_id } => {
569+
let title = CapWindowId::CaptureArea.title();
570+
let should_protect = should_protect_window(app, &title);
571+
553572
let mut window_builder = self
554573
.window_builder(app, "/capture-area")
555574
.maximized(false)
556575
.fullscreen(false)
557576
.shadow(false)
558577
.always_on_top(true)
559-
.content_protected(true)
578+
.content_protected(should_protect)
560579
.skip_taskbar(true)
561580
.closable(true)
562581
.decorations(false)
@@ -604,6 +623,9 @@ impl ShowCapWindow {
604623
let width = 250.0;
605624
let height = 40.0;
606625

626+
let title = CapWindowId::InProgressRecording.title();
627+
let should_protect = should_protect_window(app, &title);
628+
607629
let window = self
608630
.window_builder(app, "/in-progress-recording")
609631
.maximized(false)
@@ -613,7 +635,7 @@ impl ShowCapWindow {
613635
.always_on_top(true)
614636
.transparent(true)
615637
.visible_on_all_workspaces(true)
616-
.content_protected(true)
638+
.content_protected(should_protect)
617639
.inner_size(width, height)
618640
.position(
619641
((monitor.size().width as f64) / monitor.scale_factor() - width) / 2.0,
@@ -634,6 +656,9 @@ impl ShowCapWindow {
634656
window
635657
}
636658
Self::RecordingsOverlay => {
659+
let title = CapWindowId::RecordingsOverlay.title();
660+
let should_protect = should_protect_window(app, &title);
661+
637662
let window = self
638663
.window_builder(app, "/recordings-overlay")
639664
.maximized(false)
@@ -643,7 +668,7 @@ impl ShowCapWindow {
643668
.always_on_top(true)
644669
.visible_on_all_workspaces(true)
645670
.accept_first_mouse(true)
646-
.content_protected(true)
671+
.content_protected(should_protect)
647672
.inner_size(
648673
(monitor.size().width as f64) / monitor.scale_factor(),
649674
(monitor.size().height as f64) / monitor.scale_factor(),
@@ -840,6 +865,34 @@ fn position_traffic_lights_impl(
840865
.ok();
841866
}
842867

868+
fn should_protect_window(app: &AppHandle<Wry>, window_title: &str) -> bool {
869+
let matches = |list: &[WindowExclusion]| {
870+
list.iter()
871+
.any(|entry| entry.matches(None, None, Some(window_title)))
872+
};
873+
874+
GeneralSettingsStore::get(app)
875+
.ok()
876+
.flatten()
877+
.map(|settings| matches(&settings.excluded_windows))
878+
.unwrap_or_else(|| matches(&general_settings::default_excluded_windows()))
879+
}
880+
881+
#[tauri::command]
882+
#[specta::specta]
883+
pub fn refresh_window_content_protection(app: AppHandle<Wry>) -> Result<(), String> {
884+
for (label, window) in app.webview_windows() {
885+
if let Ok(id) = CapWindowId::from_str(&label) {
886+
let title = id.title();
887+
window
888+
.set_content_protected(should_protect_window(&app, &title))
889+
.map_err(|e| e.to_string())?;
890+
}
891+
}
892+
893+
Ok(())
894+
}
895+
843896
// Credits: tauri-plugin-window-state
844897
trait MonitorExt {
845898
fn intersects(

0 commit comments

Comments
 (0)