Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0a5fea6
Refactor project config save with custom debounce
richiemcilroy Nov 17, 2025
bd1fc73
fmt
richiemcilroy Nov 17, 2025
b61958c
Merge branch 'main' into editor-perf
richiemcilroy Nov 17, 2025
5d95f24
Handle excluded windows in macOS screen capture
richiemcilroy Nov 17, 2025
dbe8ae1
Refactor waveform rendering in ClipTrack
richiemcilroy Nov 17, 2025
cced653
Improve camera frame forwarding and logging
richiemcilroy Nov 17, 2025
37eb7c8
Update settings.local.json
richiemcilroy Nov 17, 2025
8b403a8
Update recording UI container styles
richiemcilroy Nov 18, 2025
184ad59
lost a days work (I F'D UP)
richiemcilroy Nov 18, 2025
fee3391
Reset camera and mic state on window close and recording end
richiemcilroy Nov 18, 2025
d2b8fd4
Update close button to use window close action
richiemcilroy Nov 18, 2025
6aca28e
Add initializing state to recording flow
richiemcilroy Nov 18, 2025
41fbc5c
Improve camera initialization and recording state handling
richiemcilroy Nov 18, 2025
44a5348
Refactor type casting for currentRecording data
richiemcilroy Nov 18, 2025
6dd3ff1
Hide native camera preview toggle on Windows
richiemcilroy Nov 18, 2025
ce63e02
Add resizing to camera window
richiemcilroy Nov 18, 2025
dd39425
Set Mellow as default for CursorAnimationStyle
richiemcilroy Nov 18, 2025
831fb76
Fix import order in experimental settings route
richiemcilroy Nov 18, 2025
891b122
Improve camera feed sender handling and logging
richiemcilroy Nov 18, 2025
2b504dd
Update camera.tsx
richiemcilroy Nov 18, 2025
d24e92c
Add code formatting guidelines to documentation
richiemcilroy Nov 18, 2025
e6e9320
Improve logging and error context in recording pipeline
richiemcilroy Nov 18, 2025
5de394f
Update apps/desktop/src-tauri/src/camera.rs
richiemcilroy Nov 18, 2025
0066eb1
Add camera overlay bounds update and revert logic
richiemcilroy Nov 18, 2025
dec0d8e
Merge branch 'editor-perf' of https://github.com/CapSoftware/Cap into…
richiemcilroy Nov 18, 2025
05f39bb
Improve countdown animation and fix recording logic
richiemcilroy Nov 18, 2025
fd30b14
Add checked_duration_since for timestamp types
richiemcilroy Nov 18, 2025
a0aeffa
Refactor duration calculation for readability
richiemcilroy Nov 18, 2025
51e2f6e
misc packages
richiemcilroy Nov 18, 2025
6db1d2d
Remove unused setCamera mutation in camera page
richiemcilroy Nov 18, 2025
ff78eba
Clarify and emphasize no code comments policy
richiemcilroy Nov 19, 2025
756d296
Improve camera window resizing and positioning logic
richiemcilroy Nov 19, 2025
0510be8
Add __CAP__ property to Window interface
richiemcilroy Nov 19, 2025
695bb66
Fix requestAnimationFrame cleanup in overlay
richiemcilroy Nov 19, 2025
ef920b2
gen'd files
richiemcilroy Nov 19, 2025
2f75043
clippy bits
richiemcilroy Nov 19, 2025
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
56 changes: 34 additions & 22 deletions apps/desktop/src-tauri/src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,9 @@
// Basically poor man's MSAA
static GPU_SURFACE_SCALE: u32 = 4;

#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Type)]
#[serde(rename_all = "lowercase")]
pub enum CameraPreviewSize {
#[default]
Sm,
Lg,
}
pub const MIN_CAMERA_SIZE: f32 = 150.0;
pub const MAX_CAMERA_SIZE: f32 = 600.0;
pub const DEFAULT_CAMERA_SIZE: f32 = 230.0;

#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Type)]
#[serde(rename_all = "lowercase")]
Expand All @@ -45,13 +41,27 @@
Full,
}

#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Type)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct CameraPreviewState {
size: CameraPreviewSize,
size: f32,
shape: CameraPreviewShape,
mirrored: bool,
}

impl Default for CameraPreviewState {
fn default() -> Self {
Self {
size: DEFAULT_CAMERA_SIZE,
shape: CameraPreviewShape::default(),
mirrored: false,
}
}
}

fn clamp_size(size: f32) -> f32 {
size.max(MIN_CAMERA_SIZE).min(MAX_CAMERA_SIZE)

Check failure on line 62 in apps/desktop/src-tauri/src/camera.rs

View workflow job for this annotation

GitHub Actions / Clippy (aarch64-apple-darwin, macos-latest)

clamp-like pattern without using clamp function

error: clamp-like pattern without using clamp function --> apps/desktop/src-tauri/src/camera.rs:62:5 | 62 | size.max(MIN_CAMERA_SIZE).min(MAX_CAMERA_SIZE) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace with clamp: `size.clamp(MIN_CAMERA_SIZE, MAX_CAMERA_SIZE)` | = note: clamp will panic if max < min, min.is_nan(), or max.is_nan() = note: clamp returns NaN if the input is NaN = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.91.0/index.html#manual_clamp = note: requested on the command line with `-D clippy::manual-clamp`
}

pub struct CameraPreviewManager {
store: Result<Arc<tauri_plugin_store::Store<tauri::Wry>>, String>,
preview: Option<InitializedCameraPreview>,
Expand All @@ -70,17 +80,22 @@

/// Get the current state of the camera window.
pub fn get_state(&self) -> anyhow::Result<CameraPreviewState> {
Ok(self
let mut state: CameraPreviewState = self
.store
.as_ref()
.map_err(|err| anyhow!("{err}"))?
.get("state")
.and_then(|v| serde_json::from_value(v).ok().unwrap_or_default())
.unwrap_or_default())
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default();

state.size = clamp_size(state.size);
Ok(state)
}

/// Save the current state of the camera window.
pub fn set_state(&self, state: CameraPreviewState) -> anyhow::Result<()> {
pub fn set_state(&self, mut state: CameraPreviewState) -> anyhow::Result<()> {
state.size = clamp_size(state.size);

let store = self.store.as_ref().map_err(|err| anyhow!("{err}"))?;
store.set("state", serde_json::to_value(&state)?);
store.save()?;
Expand Down Expand Up @@ -607,16 +622,17 @@

/// Update the uniforms which hold the camera preview state
fn update_state_uniforms(&self, state: &CameraPreviewState) {
let clamped_size = clamp_size(state.size);
let normalized_size =
(clamped_size - MIN_CAMERA_SIZE) / (MAX_CAMERA_SIZE - MIN_CAMERA_SIZE);

let state_uniforms = StateUniforms {
shape: match state.shape {
CameraPreviewShape::Round => 0.0,
CameraPreviewShape::Square => 1.0,
CameraPreviewShape::Full => 2.0,
},
size: match state.size {
CameraPreviewSize::Sm => 0.0,
CameraPreviewSize::Lg => 1.0,
},
size: normalized_size,
mirrored: if state.mirrored { 1.0 } else { 0.0 },
_padding: 0.0,
};
Expand Down Expand Up @@ -664,11 +680,7 @@
) -> tauri::Result<(u32, u32)> {
trace!("CameraPreview/resize_window");

let base: f32 = if state.size == CameraPreviewSize::Sm {
230.0
} else {
400.0
};
let base = clamp_size(state.size);
let window_width = if state.shape == CameraPreviewShape::Full {
if aspect >= 1.0 { base * aspect } else { base }
} else {
Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src-tauri/src/camera.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {

} else if (shape == 1.0) {
// Square shape with enhanced corner anti-aliasing
let corner_radius = select(0.1, 0.12, size == 1.0);
// Interpolate corner radius based on normalized size (0-1)
let corner_radius = mix(0.10, 0.14, size);
let abs_uv = abs(center_uv);
let corner_pos = abs_uv - (1.0 - corner_radius);
let corner_dist = length(max(corner_pos, vec2<f32>(0.0, 0.0)));
Expand All @@ -138,7 +139,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
} else if (shape == 2.0) {
// Full shape with aspect ratio-corrected rounded corners
let window_aspect = window_uniforms.window_width / window_uniforms.window_height;
let corner_radius = select(0.08, 0.1, size == 1.0); // radius based on size (8% for small, 10% for large)
// Interpolate corner radius based on normalized size (0-1)
let corner_radius = mix(0.08, 0.12, size);

let abs_uv = abs(center_uv);
let corner_pos = abs_uv - (1.0 - corner_radius);
Expand Down
9 changes: 3 additions & 6 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager, Url};
use tracing::trace;

use crate::{
App, ArcLock, apply_camera_input, apply_mic_input, recording::StartRecordingInputs,
windows::ShowCapWindow,
};
use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -119,8 +116,8 @@ impl DeepLinkAction {
} => {
let state = app.state::<ArcLock<App>>();

apply_camera_input(app.clone(), state.clone(), camera).await?;
apply_mic_input(state.clone(), mic_label).await?;
crate::set_camera_input(app.clone(), state.clone(), camera).await?;
crate::set_mic_input(state.clone(), mic_label).await?;

let capture_target: ScreenCaptureTarget = match capture_mode {
CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays()
Expand Down
100 changes: 68 additions & 32 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,6 @@ impl App {
#[specta::specta]
#[instrument(skip(state))]
async fn set_mic_input(state: MutableState<'_, App>, label: Option<String>) -> Result<(), String> {
apply_mic_input(state, label).await
}

pub(crate) async fn apply_mic_input(
state: MutableState<'_, App>,
label: Option<String>,
) -> Result<(), String> {
let (mic_feed, studio_handle, current_label) = {
let app = state.read().await;
let handle = match app.current_recording() {
Expand Down Expand Up @@ -421,14 +414,6 @@ async fn set_camera_input(
app_handle: AppHandle,
state: MutableState<'_, App>,
id: Option<DeviceOrModelID>,
) -> Result<(), String> {
apply_camera_input(app_handle, state, id).await
}

pub(crate) async fn apply_camera_input(
app_handle: AppHandle,
state: MutableState<'_, App>,
id: Option<DeviceOrModelID>,
) -> Result<(), String> {
let app = state.read().await;
let camera_feed = app.camera_feed.clone();
Expand Down Expand Up @@ -459,12 +444,39 @@ pub(crate) async fn apply_camera_input(
.map_err(|e| e.to_string())?;
}
Some(id) => {
camera_feed
.ask(feeds::camera::SetInput { id: id.clone() })
.await
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
let mut attempts = 0;
loop {
attempts += 1;

// We first ask the actor to set the input
// This returns a future that resolves when the camera is actually ready
let request = camera_feed
.ask(feeds::camera::SetInput { id: id.clone() })
.await
.map_err(|e| e.to_string());

let result = match request {
Ok(future) => future.await.map_err(|e| e.to_string()),
Err(e) => Err(e),
};

match result {
Ok(_) => break,
Err(e) => {
if attempts >= 3 {
return Err(format!(
"Failed to initialize camera after {} attempts: {}",
attempts, e
));
}
warn!(
"Failed to set camera input (attempt {}): {}. Retrying...",
attempts, e
);
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}

ShowCapWindow::Camera
.show(&app_handle)
Expand Down Expand Up @@ -741,11 +753,19 @@ enum CurrentRecordingTarget {
},
}

#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub enum RecordingStatus {
Pending,
Recording,
}

#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
struct CurrentRecording {
target: CurrentRecordingTarget,
mode: RecordingMode,
status: RecordingStatus,
}

#[tauri::command]
Expand All @@ -756,10 +776,14 @@ async fn get_current_recording(
) -> Result<JsonValue<Option<CurrentRecording>>, ()> {
let state = state.read().await;

let (mode, capture_target) = match &state.recording_state {
let (mode, capture_target, status) = match &state.recording_state {
RecordingState::None => return Ok(JsonValue::new(&None)),
RecordingState::Pending { mode, target } => (*mode, target),
RecordingState::Active(inner) => (inner.mode(), inner.capture_target()),
RecordingState::Pending { mode, target } => (*mode, target, RecordingStatus::Pending),
RecordingState::Active(inner) => (
inner.mode(),
inner.capture_target(),
RecordingStatus::Recording,
),
};

let target = match capture_target {
Expand All @@ -777,7 +801,11 @@ async fn get_current_recording(
},
};

Ok(JsonValue::new(&Some(CurrentRecording { target, mode })))
Ok(JsonValue::new(&Some(CurrentRecording {
target,
mode,
status,
})))
}

#[derive(Serialize, Type, tauri_specta::Event, Clone)]
Expand Down Expand Up @@ -2558,8 +2586,8 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
.flatten()
.unwrap_or_default();

let _ = apply_mic_input(app.state(), settings.mic_name).await;
let _ = apply_camera_input(app.clone(), app.state(), settings.camera_id).await;
let _ = set_mic_input(app.state(), settings.mic_name).await;
let _ = set_camera_input(app.clone(), app.state(), settings.camera_id).await;

let _ = start_recording(app.clone(), app.state(), {
recording::StartRecordingInputs {
Expand Down Expand Up @@ -2635,6 +2663,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
.camera_feed
.ask(feeds::camera::RemoveInput)
.await;

app_state.selected_mic_label = None;
app_state.selected_camera_id = None;
app_state.camera_in_use = false;
Expand Down Expand Up @@ -2695,11 +2724,18 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
CapWindowId::Camera => {
let app = app.clone();
tokio::spawn(async move {
app.state::<ArcLock<App>>()
.write()
.await
.camera_preview
.on_window_close();
let state = app.state::<ArcLock<App>>();
let mut app_state = state.write().await;

app_state.camera_preview.on_window_close();

if !app_state.is_recording_active_or_pending() {
let _ = app_state
.camera_feed
.ask(feeds::camera::RemoveInput)
.await;
app_state.camera_in_use = false;
}
});
}
_ => {}
Expand Down
Loading
Loading