Skip to content
Closed
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
3 changes: 1 addition & 2 deletions apps/desktop/src-tauri/src/general_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ fn default_enable_native_camera_preview() -> bool {
}

fn default_enable_new_recording_flow() -> bool {
false
// cfg!(debug_assertions)
cfg!(debug_assertions)
}

fn no(_: &bool) -> bool {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
captions::export_captions_srt,
target_select_overlay::open_target_select_overlays,
target_select_overlay::close_target_select_overlays,
target_select_overlay::display_information,
])
.events(tauri_specta::collect_events![
RecordingOptionsChanged,
Expand Down
78 changes: 49 additions & 29 deletions apps/desktop/src-tauri/src/target_select_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use base64::prelude::*;

use crate::windows::{CapWindowId, ShowCapWindow};
use cap_displays::{
DisplayId, WindowId,
Display, DisplayId, WindowId,
bounds::{LogicalBounds, PhysicalSize},
};
use serde::Serialize;
Expand All @@ -24,7 +24,6 @@ use tracing::error;
pub struct TargetUnderCursor {
display_id: Option<DisplayId>,
window: Option<WindowUnderCursor>,
screen: Option<ScreenUnderCursor>,
}

#[derive(Serialize, Type, Clone)]
Expand All @@ -36,7 +35,7 @@ pub struct WindowUnderCursor {
}

#[derive(Serialize, Type, Clone)]
pub struct ScreenUnderCursor {
pub struct DisplayInformation {
name: String,
physical_size: PhysicalSize,
refresh_rate: String,
Expand All @@ -62,29 +61,38 @@ pub async fn open_target_select_overlays(
let app = app.clone();
async move {
loop {
let display = cap_displays::Display::get_containing_cursor();
let window = cap_displays::Window::get_topmost_at_cursor();

let _ = TargetUnderCursor {
display_id: display.map(|d| d.id()),
window: window.and_then(|w| {
Some(WindowUnderCursor {
id: w.id(),
bounds: w.bounds()?,
app_name: w.owner_name()?,
icon: w.app_icon().map(|bytes| {
format!("data:image/png;base64,{}", BASE64_STANDARD.encode(&bytes))
}),
})
}),
screen: display.map(|d| ScreenUnderCursor {
name: d.name(),
physical_size: d.physical_size(),
refresh_rate: d.refresh_rate().to_string(),
}),
{
let display = cap_displays::Display::get_containing_cursor();
let window = cap_displays::Window::get_topmost_at_cursor();

let _ = TargetUnderCursor {
display_id: display.map(|d| d.id()),
window: window.and_then(|w| {
let bounds = w.bounds()?;

// Convert global window bounds to display-relative coordinates
let bounds = if let Some(current_display) = &display {
let display_pos = current_display.raw_handle().logical_position();
LogicalBounds::new(bounds.position() - display_pos, bounds.size())
} else {
bounds
};

Some(WindowUnderCursor {
id: w.id(),
bounds,
app_name: w.owner_name()?,
icon: w.app_icon().map(|bytes| {
format!(
"data:image/png;base64,{}",
BASE64_STANDARD.encode(&bytes)
)
}),
})
}),
}
.emit(&app);
}
.emit(&app);

tokio::time::sleep(Duration::from_millis(50)).await;
}
}
Expand All @@ -110,10 +118,7 @@ pub async fn open_target_select_overlays(

#[specta::specta]
#[tauri::command]
pub async fn close_target_select_overlays(
app: AppHandle,
// state: tauri::State<'_, WindowFocusManager>,
) -> Result<(), String> {
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();
Expand All @@ -123,6 +128,21 @@ pub async fn close_target_select_overlays(
Ok(())
}

#[specta::specta]
#[tauri::command]
pub async fn display_information(display_id: &str) -> Result<DisplayInformation, String> {
let display_id = display_id
.parse::<DisplayId>()
.map_err(|err| format!("Invalid display ID: {}", err))?;
let display = Display::from_id(display_id).ok_or("Display not found")?;

Ok(DisplayInformation {
name: display.name().to_string(),
physical_size: display.physical_size(),
refresh_rate: display.refresh_rate().to_string(),
})
}

// Windows doesn't have a proper concept of window z-index's so we implement them in userspace :(
#[derive(Default)]
pub struct WindowFocusManager {
Expand Down
95 changes: 92 additions & 3 deletions apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,61 @@ impl ShowCapWindow {
}
}
Self::TargetSelectOverlay { display_id } => {
println!(
"SPAWNING TARGET SELECT OVERLAY {:?}",
display_id.to_string()
);

let Some(display) = cap_displays::Display::from_id(display_id.clone()) else {
return Err(tauri::Error::WindowNotFound);
};

// Get the size and position, then find the correct monitor for scale factor
let size = display.raw_handle().logical_size();
let position = display.raw_handle().logical_position();

println!(
"\t TSO: {:?} LogicalSize {{ width: {}, height: {} }} LogicalPosition {{ x: {}, y: {} }}",
display_id.to_string(),
size.width(),
size.height(),
position.x(),
position.y()
);

// Get the target monitor for proper scale factor handling
let target_monitor = app
.monitor_from_point(position.x(), position.y())
.ok()
.flatten()
.unwrap_or(monitor);

let scale_factor = target_monitor.scale_factor();
let monitor_size = target_monitor.size();
let monitor_pos = target_monitor.position();

// For scale factor 1 monitors, use the monitor's actual size to avoid Tauri scaling issues
let scaled_size = if scale_factor == 1.0 {
(monitor_size.width as f64, monitor_size.height as f64)
} else {
// Use properly scaled dimensions like CaptureArea does for high-DPI monitors
(size.width() / scale_factor, size.height() / scale_factor)
};
let position_tuple = (position.x(), position.y());

println!(
"\t TSO Scale Factor: {} -> Scaled Size: {}x{}",
scale_factor, scaled_size.0, scaled_size.1
);
println!(
"\t TSO Monitor: {}x{} at ({}, {}) - Scale: {}",
monitor_size.width,
monitor_size.height,
monitor_pos.x,
monitor_pos.y,
scale_factor
);

let mut window_builder = self
.window_builder(
app,
Expand All @@ -267,15 +315,56 @@ impl ShowCapWindow {
.resizable(false)
.fullscreen(false)
.shadow(false)
.always_on_top(cfg!(target_os = "macos"))
.content_protected(true)
.always_on_top(true)
.visible_on_all_workspaces(true)
.skip_taskbar(true)
.inner_size(size.width(), size.height())
.position(position.x(), position.y())
.transparent(true);

// On Windows, set minimal size during creation, then resize after
#[cfg(target_os = "windows")]
{
window_builder = window_builder.inner_size(100.0, 100.0).position(0.0, 0.0);
}

// On other platforms, set size and position during creation as before
#[cfg(not(target_os = "windows"))]
{
window_builder = window_builder
.inner_size(scaled_size.0, scaled_size.1)
.position(position_tuple.0, position_tuple.1);
}

let window = window_builder.build()?;

// On Windows, manually set size and position after window creation
#[cfg(target_os = "windows")]
{
use tauri::{LogicalPosition, LogicalSize};
// Small delay to ensure window is fully initialized
std::thread::sleep(std::time::Duration::from_millis(10));
let _ = window.set_size(LogicalSize::new(scaled_size.0, scaled_size.1));
let _ = window
.set_position(LogicalPosition::new(position_tuple.0, position_tuple.1));
}

if let (Ok(final_pos), Ok(final_size)) =
(window.inner_position(), window.inner_size())
{
println!(
"Final Position for {:?}: ({}, {})",
display_id.to_string(),
final_pos.x,
final_pos.y
);
println!(
"Final Size for {:?}: ({}, {})",
display_id.to_string(),
final_size.width,
final_size.height
);
}

app.state::<WindowFocusManager>()
.spawn(display_id, window.clone());

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions apps/desktop/src/routes/(window-chrome)/CameraSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createQuery } from "@tanstack/solid-query";
import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { trackEvent } from "~/utils/analytics";
import { createCurrentRecordingQuery, getPermissions } from "~/utils/queries";
import type { CameraInfo } from "~/utils/tauri";
import TargetSelectInfoPill from "./TargetSelectInfoPill";
import useRequestPermission from "./useRequestPermission";

const NO_CAMERA = "No Camera";

export default function CameraSelect(props: {
disabled?: boolean;
options: CameraInfo[];
value: CameraInfo | null;
onChange: (camera: CameraInfo | null) => void;
}) {
const currentRecording = createCurrentRecordingQuery();
const permissions = createQuery(() => getPermissions);
const requestPermission = useRequestPermission();

const permissionGranted = () =>
permissions?.data?.camera === "granted" ||
permissions?.data?.camera === "notNeeded";

const onChange = (cameraLabel: CameraInfo | null) => {
if (!cameraLabel && permissions?.data?.camera !== "granted")
return requestPermission("camera");

props.onChange(cameraLabel);

trackEvent("camera_selected", {
camera_name: cameraLabel?.display_name ?? null,
enabled: !!cameraLabel,
});
};

return (
<div class="flex flex-col gap-[0.25rem] items-stretch text-[--text-primary]">
<button
disabled={!!currentRecording.data || props.disabled}
class="flex flex-row gap-2 items-center px-2 w-full h-9 rounded-lg transition-colors hover:bg-gray-3 bg-gray-2 disabled:text-gray-11 KSelect"
onClick={() => {
Promise.all([
CheckMenuItem.new({
text: NO_CAMERA,
checked: props.value === null,
action: () => onChange(null),
}),
PredefinedMenuItem.new({ item: "Separator" }),
...props.options.map((o) =>
CheckMenuItem.new({
text: o.display_name,
checked: o === props.value,
action: () => onChange(o),
}),
),
])
.then((items) => Menu.new({ items }))
.then((m) => {
m.popup();
});
}}
>
<IconCapCamera class="text-gray-10 size-4" />
<p class="flex-1 text-sm text-left truncate">
{props.value?.display_name ?? NO_CAMERA}
</p>
<TargetSelectInfoPill
value={props.value}
permissionGranted={permissionGranted()}
requestPermission={() => requestPermission("camera")}
onClick={(e) => {
if (!props.options) return;
if (props.value !== null) {
e.stopPropagation();
props.onChange(null);
}
}}
/>
</button>
</div>
);
}
Loading
Loading