From 5d54edd6dad426c213835e5ed9e610c96d9d185b Mon Sep 17 00:00:00 2001 From: namearth5005 Date: Wed, 18 Mar 2026 14:16:23 +0700 Subject: [PATCH 1/6] Add AutoZoomConfig struct to extract hardcoded zoom constants into configurable fields --- crates/project/src/configuration.rs | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/project/src/configuration.rs b/crates/project/src/configuration.rs index f2f49fbea0..1a42f4dcd6 100644 --- a/crates/project/src/configuration.rs +++ b/crates/project/src/configuration.rs @@ -455,6 +455,40 @@ impl Default for ScreenMovementSpring { } } +#[derive(Type, Serialize, Deserialize, Clone, Copy, Debug)] +#[serde(rename_all = "camelCase", default)] +pub struct AutoZoomConfig { + pub zoom_amount: f64, + pub click_group_time_threshold: f64, + pub click_group_spatial_threshold: f64, + pub click_pre_padding: f64, + pub click_post_padding: f64, + pub movement_pre_padding: f64, + pub movement_post_padding: f64, + pub merge_gap_threshold: f64, + pub min_segment_duration: f64, + pub movement_event_distance_threshold: f64, + pub movement_window_distance_threshold: f64, +} + +impl Default for AutoZoomConfig { + fn default() -> Self { + Self { + zoom_amount: 1.5, + click_group_time_threshold: 2.5, + click_group_spatial_threshold: 0.15, + click_pre_padding: 0.4, + click_post_padding: 1.8, + movement_pre_padding: 0.3, + movement_post_padding: 1.5, + merge_gap_threshold: 0.8, + min_segment_duration: 1.0, + movement_event_distance_threshold: 0.02, + movement_window_distance_threshold: 0.08, + } + } +} + impl CursorAnimationStyle { pub fn preset(self) -> Option { match self { From ae3905272e4f7943a62cb70c2d72a554d129fa83 Mon Sep 17 00:00:00 2001 From: namearth5005 Date: Wed, 18 Mar 2026 14:18:49 +0700 Subject: [PATCH 2/6] feat(settings): add auto_zoom_config to GeneralSettingsStore with serde defaults --- apps/desktop/src-tauri/src/general_settings.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index 546b4960a8..cc5789d9fe 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -124,6 +124,8 @@ pub struct GeneralSettingsStore { #[serde(default)] pub auto_zoom_on_clicks: bool, #[serde(default)] + pub auto_zoom_config: cap_project::AutoZoomConfig, + #[serde(default)] pub post_deletion_behaviour: PostDeletionBehaviour, #[serde(default = "default_excluded_windows")] pub excluded_windows: Vec, @@ -203,6 +205,7 @@ impl Default for GeneralSettingsStore { recording_countdown: Some(3), enable_native_camera_preview: default_enable_native_camera_preview(), auto_zoom_on_clicks: false, + auto_zoom_config: cap_project::AutoZoomConfig::default(), post_deletion_behaviour: PostDeletionBehaviour::DoNothing, excluded_windows: default_excluded_windows(), delete_instant_recordings_after_upload: false, From 04a7fed46afebd907008d2c06d6ebafd81471d7a Mon Sep 17 00:00:00 2001 From: namearth5005 Date: Wed, 18 Mar 2026 14:23:53 +0700 Subject: [PATCH 3/6] feat(recording): wire AutoZoomConfig into zoom segment generation, replacing hardcoded constants --- apps/desktop/src-tauri/src/lib.rs | 6 +- apps/desktop/src-tauri/src/recording.rs | 88 ++++++++++++++----------- 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 9918b2e0cd..7c9e447e92 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2118,7 +2118,11 @@ async fn generate_zoom_segments_from_clicks( let meta = editor_instance.meta(); let recordings = &editor_instance.recordings; - let zoom_segments = recording::generate_zoom_segments_for_project(meta, recordings); + let zoom_segments = recording::generate_zoom_segments_for_project( + meta, + recordings, + &cap_project::AutoZoomConfig::default(), + ); Ok(zoom_segments) } diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index d65bce27fc..687bc75cb3 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -1989,30 +1989,29 @@ async fn finalize_studio_recording( Ok(()) } -/// Core logic for generating zoom segments based on mouse click events. -/// This is an experimental feature that automatically creates zoom effects -/// around user interactions to highlight important moments. fn generate_zoom_segments_from_clicks_impl( mut clicks: Vec, mut moves: Vec, max_duration: f64, + config: &cap_project::AutoZoomConfig, ) -> Vec { const STOP_PADDING_SECONDS: f64 = 0.5; - const CLICK_GROUP_TIME_THRESHOLD_SECS: f64 = 2.5; - const CLICK_GROUP_SPATIAL_THRESHOLD: f64 = 0.15; - const CLICK_PRE_PADDING: f64 = 0.4; - const CLICK_POST_PADDING: f64 = 1.8; - const MOVEMENT_PRE_PADDING: f64 = 0.3; - const MOVEMENT_POST_PADDING: f64 = 1.5; - const MERGE_GAP_THRESHOLD: f64 = 0.8; - const MIN_SEGMENT_DURATION: f64 = 1.0; const MOVEMENT_WINDOW_SECONDS: f64 = 1.5; - const MOVEMENT_EVENT_DISTANCE_THRESHOLD: f64 = 0.02; - const MOVEMENT_WINDOW_DISTANCE_THRESHOLD: f64 = 0.08; - const AUTO_ZOOM_AMOUNT: f64 = 1.5; const SHAKE_FILTER_THRESHOLD: f64 = 0.33; const SHAKE_FILTER_WINDOW_MS: f64 = 150.0; + let click_group_time_threshold_secs = config.click_group_time_threshold; + let click_group_spatial_threshold = config.click_group_spatial_threshold; + let click_pre_padding = config.click_pre_padding; + let click_post_padding = config.click_post_padding; + let movement_pre_padding = config.movement_pre_padding; + let movement_post_padding = config.movement_post_padding; + let merge_gap_threshold = config.merge_gap_threshold; + let min_segment_duration = config.min_segment_duration; + let movement_event_distance_threshold = config.movement_event_distance_threshold; + let movement_window_distance_threshold = config.movement_window_distance_threshold; + let auto_zoom_amount = config.zoom_amount; + if max_duration <= 0.0 { return Vec::new(); } @@ -2076,13 +2075,13 @@ fn generate_zoom_segments_from_clicks_impl( let can_join = group.iter().any(|&group_idx| { let group_click = &clicks[group_idx]; let group_time = group_click.time_ms / 1000.0; - let time_close = (click_time - group_time).abs() < CLICK_GROUP_TIME_THRESHOLD_SECS; + let time_close = (click_time - group_time).abs() < click_group_time_threshold_secs; let spatial_close = match (click_pos, click_positions.get(&group_idx)) { (Some((x1, y1)), Some((x2, y2))) => { let dx = x1 - x2; let dy = y1 - y2; - (dx * dx + dy * dy).sqrt() < CLICK_GROUP_SPATIAL_THRESHOLD + (dx * dx + dy * dy).sqrt() < click_group_spatial_threshold } _ => true, }; @@ -2116,8 +2115,8 @@ fn generate_zoom_segments_from_clicks_impl( let group_start = times.iter().cloned().fold(f64::INFINITY, f64::min); let group_end = times.iter().cloned().fold(f64::NEG_INFINITY, f64::max); - let start = (group_start - CLICK_PRE_PADDING).max(0.0); - let end = (group_end + CLICK_POST_PADDING).min(activity_end_limit); + let start = (group_start - click_pre_padding).max(0.0); + let end = (group_end + click_post_padding).min(activity_end_limit); if end > start { intervals.push((start, end)); @@ -2199,15 +2198,15 @@ fn generate_zoom_segments_from_clicks_impl( window_distance = 0.0; } - let significant_movement = distance >= MOVEMENT_EVENT_DISTANCE_THRESHOLD - || window_distance >= MOVEMENT_WINDOW_DISTANCE_THRESHOLD; + let significant_movement = distance >= movement_event_distance_threshold + || window_distance >= movement_window_distance_threshold; if !significant_movement { continue; } - let start = (time - MOVEMENT_PRE_PADDING).max(0.0); - let end = (time + MOVEMENT_POST_PADDING).min(activity_end_limit); + let start = (time - movement_pre_padding).max(0.0); + let end = (time + movement_post_padding).min(activity_end_limit); if end > start { intervals.push((start, end)); @@ -2223,7 +2222,7 @@ fn generate_zoom_segments_from_clicks_impl( let mut merged: Vec<(f64, f64)> = Vec::new(); for interval in intervals { if let Some(last) = merged.last_mut() - && interval.0 <= last.1 + MERGE_GAP_THRESHOLD + && interval.0 <= last.1 + merge_gap_threshold { last.1 = last.1.max(interval.1); continue; @@ -2235,14 +2234,14 @@ fn generate_zoom_segments_from_clicks_impl( .into_iter() .filter_map(|(start, end)| { let duration = end - start; - if duration < MIN_SEGMENT_DURATION { + if duration < min_segment_duration { return None; } Some(ZoomSegment { start, end, - amount: AUTO_ZOOM_AMOUNT, + amount: auto_zoom_amount, mode: ZoomMode::Auto, glide_direction: GlideDirection::None, glide_speed: 0.5, @@ -2253,13 +2252,11 @@ fn generate_zoom_segments_from_clicks_impl( .collect() } -/// Generates zoom segments based on mouse click events during recording. -/// Used during the recording completion process. pub fn generate_zoom_segments_from_clicks( recording: &studio_recording::CompletedRecording, recordings: &ProjectRecordingsMeta, + config: &cap_project::AutoZoomConfig, ) -> Vec { - // Build a temporary RecordingMeta so we can use the common implementation let recording_meta = RecordingMeta { platform: None, project_path: recording.project_path.clone(), @@ -2269,14 +2266,13 @@ pub fn generate_zoom_segments_from_clicks( upload: None, }; - generate_zoom_segments_for_project(&recording_meta, recordings) + generate_zoom_segments_for_project(&recording_meta, recordings, config) } -/// Generates zoom segments from clicks for an existing project. -/// Used in the editor context where we have RecordingMeta. pub fn generate_zoom_segments_for_project( recording_meta: &RecordingMeta, recordings: &ProjectRecordingsMeta, + config: &cap_project::AutoZoomConfig, ) -> Vec { let RecordingMetaInner::Studio(studio_meta) = &recording_meta.inner else { return Vec::new(); @@ -2309,7 +2305,7 @@ pub fn generate_zoom_segments_for_project( } } - generate_zoom_segments_from_clicks_impl(all_clicks, all_moves, recordings.duration()) + generate_zoom_segments_from_clicks_impl(all_clicks, all_moves, recordings.duration(), config) } fn project_config_from_recording( @@ -2355,7 +2351,11 @@ fn project_config_from_recording( .collect::>(); let zoom_segments = if settings.auto_zoom_on_clicks { - generate_zoom_segments_from_clicks(completed_recording, recordings) + generate_zoom_segments_from_clicks( + completed_recording, + recordings, + &settings.auto_zoom_config, + ) } else { Vec::new() }; @@ -2429,8 +2429,12 @@ mod tests { #[test] fn skips_trailing_stop_click() { - let segments = - generate_zoom_segments_from_clicks_impl(vec![click_event(11_900.0)], vec![], 12.0); + let segments = generate_zoom_segments_from_clicks_impl( + vec![click_event(11_900.0)], + vec![], + 12.0, + &cap_project::AutoZoomConfig::default(), + ); assert!( segments.is_empty(), @@ -2447,7 +2451,12 @@ mod tests { move_event(1_940.0, 0.74, 0.78), ]; - let segments = generate_zoom_segments_from_clicks_impl(clicks, moves, 20.0); + let segments = generate_zoom_segments_from_clicks_impl( + clicks, + moves, + 20.0, + &cap_project::AutoZoomConfig::default(), + ); assert!( !segments.is_empty(), @@ -2469,7 +2478,12 @@ mod tests { }) .collect::>(); - let segments = generate_zoom_segments_from_clicks_impl(Vec::new(), jitter_moves, 15.0); + let segments = generate_zoom_segments_from_clicks_impl( + Vec::new(), + jitter_moves, + 15.0, + &cap_project::AutoZoomConfig::default(), + ); assert!( segments.is_empty(), From b893118ba0f13d8fb4f68e396be94e69fa3d2e6e Mon Sep 17 00:00:00 2001 From: namearth5005 Date: Wed, 18 Mar 2026 14:25:40 +0700 Subject: [PATCH 4/6] feat(ipc): pass AutoZoomConfig from settings through generate_zoom_segments command --- apps/desktop/src-tauri/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 7c9e447e92..3e67aebbcf 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2111,17 +2111,21 @@ async fn update_project_config_in_memory( #[tauri::command] #[specta::specta] -#[instrument(skip(editor_instance))] +#[instrument(skip(app, editor_instance))] async fn generate_zoom_segments_from_clicks( + app: AppHandle, editor_instance: WindowEditorInstance, ) -> Result, String> { + let settings = GeneralSettingsStore::get(&app) + .unwrap_or(None) + .unwrap_or_default(); let meta = editor_instance.meta(); let recordings = &editor_instance.recordings; let zoom_segments = recording::generate_zoom_segments_for_project( meta, recordings, - &cap_project::AutoZoomConfig::default(), + &settings.auto_zoom_config, ); Ok(zoom_segments) From c86680599b44d9850f09e09926788caa56faccd2 Mon Sep 17 00:00:00 2001 From: namearth5005 Date: Wed, 18 Mar 2026 14:29:10 +0700 Subject: [PATCH 5/6] feat(ui): add zoom amount, sensitivity, and smoothing sliders for auto-zoom settings --- .../(window-chrome)/settings/experimental.tsx | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index 3d1bd22fc7..2149e5e2ca 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -1,12 +1,50 @@ +import { Slider as KSlider } from "@kobalte/core/slider"; import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import { type } from "@tauri-apps/plugin-os"; import { createResource, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { generalSettingsStore } from "~/store"; -import { commands, type GeneralSettingsStore } from "~/utils/tauri"; +import { + commands, + type AutoZoomConfig, + type GeneralSettingsStore, +} from "~/utils/tauri"; import { ToggleSettingItem } from "./Setting"; +function SettingSlider(props: { + label: string; + value: number; + onChange: (v: number) => void; + min: number; + max: number; + step: number; + format?: (v: number) => string; +}) { + return ( +
+
+ {props.label} + + {props.format ? props.format(props.value) : props.value} + +
+ props.onChange(v[0])} + minValue={props.min} + maxValue={props.max} + step={props.step} + > + + + + + +
+ ); +} + export default function ExperimentalSettings() { const [store] = createResource(() => generalSettingsStore.get()); @@ -27,9 +65,32 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { enableNativeCameraPreview: false, autoZoomOnClicks: false, custom_cursor_capture2: true, + autoZoomConfig: { + zoomAmount: 1.5, + clickGroupTimeThreshold: 2.5, + clickGroupSpatialThreshold: 0.15, + clickPrePadding: 0.4, + clickPostPadding: 1.8, + movementPrePadding: 0.3, + movementPostPadding: 1.5, + mergeGapThreshold: 0.8, + minSegmentDuration: 1.0, + movementEventDistanceThreshold: 0.02, + movementWindowDistanceThreshold: 0.08, + }, }, ); + const handleConfigChange = ( + key: K, + value: AutoZoomConfig[K], + ) => { + setSettings("autoZoomConfig", key, value); + generalSettingsStore.set({ + autoZoomConfig: { ...settings.autoZoomConfig, [key]: value }, + }); + }; + const handleChange = async ( key: K, value: (typeof settings)[K], @@ -95,6 +156,53 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { }} /> + +
+ handleConfigChange("zoomAmount", v)} + min={1.0} + max={4.0} + step={0.1} + format={(v) => `${v.toFixed(1)}x`} + /> + + handleConfigChange( + "movementWindowDistanceThreshold", + v, + ) + } + min={0.02} + max={0.2} + step={0.01} + format={(v) => { + if (v <= 0.05) return "High"; + if (v <= 0.12) return "Medium"; + return "Low"; + }} + /> + + handleConfigChange("mergeGapThreshold", v) + } + min={0.2} + max={2.0} + step={0.1} + format={(v) => `${v.toFixed(1)}s`} + /> +
+
From 680a8e5aabbb09bcbf730b4eeea1afbee80abee1 Mon Sep 17 00:00:00 2001 From: namearth5005 Date: Wed, 18 Mar 2026 14:36:19 +0700 Subject: [PATCH 6/6] style: apply cargo fmt and biome formatting --- apps/desktop/src-tauri/src/lib.rs | 7 ++----- .../(window-chrome)/settings/experimental.tsx | 19 ++++++------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 3e67aebbcf..728792d560 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2122,11 +2122,8 @@ async fn generate_zoom_segments_from_clicks( let meta = editor_instance.meta(); let recordings = &editor_instance.recordings; - let zoom_segments = recording::generate_zoom_segments_for_project( - meta, - recordings, - &settings.auto_zoom_config, - ); + let zoom_segments = + recording::generate_zoom_segments_for_project(meta, recordings, &settings.auto_zoom_config); Ok(zoom_segments) } diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index 2149e5e2ca..b5252dbd4a 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -6,8 +6,8 @@ import { createStore } from "solid-js/store"; import { generalSettingsStore } from "~/store"; import { - commands, type AutoZoomConfig, + commands, type GeneralSettingsStore, } from "~/utils/tauri"; import { ToggleSettingItem } from "./Setting"; @@ -170,14 +170,11 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { - handleConfigChange( - "movementWindowDistanceThreshold", - v, - ) + handleConfigChange("movementWindowDistanceThreshold", v) } min={0.02} max={0.2} @@ -190,12 +187,8 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { /> - handleConfigChange("mergeGapThreshold", v) - } + value={settings.autoZoomConfig?.mergeGapThreshold ?? 0.8} + onChange={(v) => handleConfigChange("mergeGapThreshold", v)} min={0.2} max={2.0} step={0.1}