diff --git a/Cargo.lock b/Cargo.lock
index 0c4ff52c2e..6775f75e7b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "cap-desktop"
-version = "0.4.8"
+version = "0.4.81"
dependencies = [
"aho-corasick",
"anyhow",
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index c65bd5c487..cacbbc3cc4 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "cap-desktop"
-version = "0.4.8"
+version = "0.4.81"
description = "Beautiful screen recordings, owned by you."
authors = ["you"]
edition = "2024"
diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs
index e2cbe9aca7..94fdda3aa5 100644
--- a/apps/desktop/src-tauri/src/general_settings.rs
+++ b/apps/desktop/src-tauri/src/general_settings.rs
@@ -217,7 +217,7 @@ impl Default for GeneralSettingsStore {
server_url: default_server_url(),
recording_countdown: Some(3),
enable_native_camera_preview: default_enable_native_camera_preview(),
- auto_zoom_on_clicks: true,
+ auto_zoom_on_clicks: false,
capture_keyboard_events: true,
post_deletion_behaviour: PostDeletionBehaviour::DoNothing,
excluded_windows: default_excluded_windows(),
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 8660425ab9..8b80b97055 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -3350,6 +3350,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
.with_denylist(&[
CapWindowId::Onboarding.label().as_str(),
CapWindowId::Main.label().as_str(),
+ CapWindowId::Settings.label().as_str(),
"window-capture-occluder",
"target-select-overlay",
CapWindowId::CaptureArea.label().as_str(),
diff --git a/apps/desktop/src-tauri/src/permissions.rs b/apps/desktop/src-tauri/src/permissions.rs
index eb36f94e6b..f765173e45 100644
--- a/apps/desktop/src-tauri/src/permissions.rs
+++ b/apps/desktop/src-tauri/src/permissions.rs
@@ -45,7 +45,7 @@ pub enum OSPermission {
#[tauri::command(async)]
#[specta::specta]
-pub fn open_permission_settings(app: tauri::AppHandle, _permission: OSPermission) {
+pub fn open_permission_settings(_app: tauri::AppHandle, _permission: OSPermission) {
#[cfg(target_os = "macos")]
{
match _permission {
@@ -77,7 +77,7 @@ pub fn open_permission_settings(app: tauri::AppHandle, _permission: OSPermission
match process {
Ok(mut process) => {
- let app = app.clone();
+ let app = _app.clone();
tokio::spawn(async move {
match tokio::task::spawn_blocking(move || process.wait()).await {
Ok(Err(err)) => {
diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs
index 5002059697..e651a1c109 100644
--- a/apps/desktop/src-tauri/src/windows.rs
+++ b/apps/desktop/src-tauri/src/windows.rs
@@ -14,8 +14,8 @@ use std::{
time::Duration,
};
use tauri::{
- AppHandle, LogicalPosition, Manager, Monitor, PhysicalPosition, PhysicalSize, WebviewUrl,
- WebviewWindow, WebviewWindowBuilder, Wry,
+ AppHandle, LogicalPosition, LogicalSize, Manager, Monitor, PhysicalPosition, PhysicalSize,
+ WebviewUrl, WebviewWindow, WebviewWindowBuilder, Wry,
};
use tauri_specta::Event;
use tokio::sync::RwLock;
@@ -377,6 +377,19 @@ fn is_position_on_any_screen(pos_x: f64, pos_y: f64) -> bool {
false
}
+fn ensure_settings_window_bounds(window: &WebviewWindow) {
+ const MIN_W: f64 = 800.0;
+ const MIN_H: f64 = 580.0;
+ let _ = window.set_min_size(Some(LogicalSize::new(MIN_W, MIN_H)));
+ if let (Ok(physical), Ok(scale)) = (window.inner_size(), window.scale_factor()) {
+ let width = physical.width as f64 / scale;
+ let height = physical.height as f64 / scale;
+ if width < MIN_W || height < MIN_H {
+ let _ = window.set_size(LogicalSize::new(width.max(MIN_W), height.max(MIN_H)));
+ }
+ }
+}
+
#[derive(Clone, Deserialize, Type)]
pub enum CapWindowId {
Main,
@@ -955,6 +968,10 @@ impl ShowCapWindow {
window.unminimize().ok();
window.set_focus().ok();
+ if let Self::Settings { .. } = self {
+ ensure_settings_window_bounds(&window);
+ }
+
if let Self::Main { init_target_mode } = self {
let _ = RequestSetTargetMode {
target_mode: *init_target_mode,
@@ -1262,7 +1279,6 @@ impl ShowCapWindow {
#[cfg(windows)]
{
- use tauri::LogicalSize;
if let Err(e) = window.set_size(LogicalSize::new(800.0, 580.0)) {
warn!("Failed to set Settings window size on Windows: {}", e);
}
@@ -1273,6 +1289,7 @@ impl ShowCapWindow {
window.show().ok();
window.set_focus().ok();
+ ensure_settings_window_bounds(&window);
window
}
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 573a7cf911..03d7cc274d 100644
--- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx
+++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx
@@ -14,7 +14,6 @@ import {
} from "@tauri-apps/api/webviewWindow";
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
import * as dialog from "@tauri-apps/plugin-dialog";
-import { type as ostype } from "@tauri-apps/plugin-os";
import * as shell from "@tauri-apps/plugin-shell";
import * as updater from "@tauri-apps/plugin-updater";
import { cx } from "cva";
@@ -1705,9 +1704,7 @@ function Page() {
class="flex flex-1 gap-1 items-center mx-2 min-w-0"
data-tauri-drag-region
>
-
-
-
+
Settings}>
@@ -1773,9 +1770,6 @@ function Page() {
)}
-
-
-
diff --git a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx
index 977248a954..b0079de9ef 100644
--- a/apps/desktop/src/routes/(window-chrome)/onboarding.tsx
+++ b/apps/desktop/src/routes/(window-chrome)/onboarding.tsx
@@ -323,7 +323,17 @@ export default function OnboardingPage() {
});
const isMacOS = createMemo(() => ostype() === "macos");
- const permissionsOnly = createMemo(() => isRevisit() && permissionsNeeded());
+ const permissionsOnly = createMemo(
+ () => isMacOS() && isRevisit() && permissionsNeeded(),
+ );
+
+ createEffect(() => {
+ ready();
+ if (!isMacOS()) {
+ setPermsGranted(true);
+ setCorePermsGranted(true);
+ }
+ });
const totalSteps = createMemo(() => {
if (permissionsOnly()) return 1;
@@ -362,11 +372,11 @@ export default function OnboardingPage() {
setIsExiting(true);
await generalSettingsStore.set({ hasCompletedStartup: true });
setTimeout(() => {
- setShowStartupOverlay(false);
- setIsExiting(false);
if (!isMacOS()) {
goToStep(1);
}
+ setShowStartupOverlay(false);
+ setIsExiting(false);
}, 600);
};
@@ -403,7 +413,7 @@ export default function OnboardingPage() {
return "Continue";
};
- const nextDisabled = () => step() === 0 && !permsGranted();
+ const nextDisabled = () => isMacOS() && step() === 0 && !permsGranted();
const handleSkipOnboarding = () => {
if (!corePermsGranted() || permissionsOnly()) return;
@@ -477,13 +487,15 @@ export default function OnboardingPage() {
-
-
-
+
+
+
+
+
diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx
index 442d28c59b..8f2a124a7a 100644
--- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx
+++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx
@@ -220,7 +220,7 @@ type CursorPresetValues = {
friction: number;
};
-const DEFAULT_CURSOR_MOTION_BLUR = 1.0;
+const DEFAULT_CURSOR_MOTION_BLUR = 0.3;
const CURSOR_TYPE_OPTIONS = [
{
@@ -242,12 +242,24 @@ const CURSOR_ANIMATION_STYLE_OPTIONS = [
description: "Relaxed easing with a gentle follow and higher inertia.",
preset: { tension: 65, mass: 1.8, friction: 16 },
},
+ {
+ value: "smooth",
+ label: "Smooth",
+ description: "Ultra-smooth cinematic feel with high damping.",
+ preset: { tension: 80, mass: 2.5, friction: 28 },
+ },
{
value: "mellow",
label: "Mellow",
description: "Balanced smoothing for everyday tutorials and walkthroughs.",
preset: { tension: 120, mass: 1.1, friction: 18 },
},
+ {
+ value: "fast",
+ label: "Fast",
+ description: "Quick, responsive smoothing for fast-paced content.",
+ preset: { tension: 380, mass: 1.0, friction: 30 },
+ },
{
value: "custom",
label: "Custom",
diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx
index 3f706fae87..c8a12a3aec 100644
--- a/apps/desktop/src/routes/editor/Editor.tsx
+++ b/apps/desktop/src/routes/editor/Editor.tsx
@@ -14,6 +14,7 @@ import {
createResource,
createSignal,
ErrorBoundary,
+ For,
Match,
on,
onCleanup,
@@ -54,9 +55,11 @@ import { Dialog, DialogContent, EditorButton, Input, Subfield } from "./ui";
const DEFAULT_TIMELINE_HEIGHT = 260;
const MIN_PLAYER_CONTENT_HEIGHT = 320;
const MIN_TIMELINE_HEIGHT = 240;
-const RESIZE_HANDLE_HEIGHT = 8;
+const RESIZE_HANDLE_HEIGHT = 16;
const MIN_PLAYER_HEIGHT = MIN_PLAYER_CONTENT_HEIGHT + RESIZE_HANDLE_HEIGHT;
+const TIMELINE_RESIZE_GRIP_MARKS = [0, 1, 2] as const;
+
function getEditorErrorMessage(error: unknown) {
return error instanceof Error ? error.message : String(error);
}
@@ -462,18 +465,27 @@ function Inner() {
diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx
index ccc854c7ac..b293893543 100644
--- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx
+++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx
@@ -1,9 +1,11 @@
+import { Button } from "@cap/ui-solid";
import { createEventListenerMap } from "@solid-primitives/event-listener";
import { Menu } from "@tauri-apps/api/menu";
import { cx } from "cva";
import { Array, Option } from "effect";
import {
batch,
+ createEffect,
createMemo,
createRoot,
createSignal,
@@ -49,6 +51,7 @@ export function ZoomTrack(props: {
editorState,
totalDuration,
projectActions,
+ meta,
} = useEditorContext();
const { duration, secsPerPixel } = useTimelineContext();
@@ -56,8 +59,27 @@ export function ZoomTrack(props: {
const [creatingSegmentViaDrag, setCreatingSegmentViaDrag] =
createSignal(false);
+ const [isGeneratingAutoZoom, setIsGeneratingAutoZoom] = createSignal(false);
+ const [isHoveringGenerateZoomButton, setIsHoveringGenerateZoomButton] =
+ createSignal(false);
+ const [
+ sessionDismissedGenerateZoomPrompt,
+ setSessionDismissedGenerateZoomPrompt,
+ ] = createSignal(false);
+
+ const hasZoomSegments = () =>
+ (project.timeline?.zoomSegments?.length ?? 0) > 0;
+
+ const hasRecordedCursorData = () => meta().hasRecordedCursorData;
+
+ createEffect(() => {
+ if (hasZoomSegments() || sessionDismissedGenerateZoomPrompt()) {
+ setIsHoveringGenerateZoomButton(false);
+ }
+ });
const handleGenerateZoomSegments = async () => {
+ setIsGeneratingAutoZoom(true);
try {
const zoomSegments = await commands.generateZoomSegmentsFromClicks();
setProject("timeline", "zoomSegments", zoomSegments);
@@ -69,6 +91,8 @@ export function ZoomTrack(props: {
}
} catch (error) {
console.error("Failed to generate zoom segments:", error);
+ } finally {
+ setIsGeneratingAutoZoom(false);
}
};
@@ -203,17 +227,16 @@ export function ZoomTrack(props: {
start: baseSegment.start,
end: Math.max(minEndTime, endTime),
amount: 1.5,
- mode: {
- manual: {
- x: 0.5,
- y: 0.5,
- },
- },
+ mode: "auto",
});
createdSegmentIndex = index;
}),
);
+ setEditorState("timeline", "selection", {
+ type: "zoom",
+ indices: [createdSegmentIndex],
+ });
});
segmentCreated = true;
};
@@ -275,13 +298,44 @@ export function ZoomTrack(props: {
}}
>
- Click to add zoom segment
-
- (Smoothly zoom in on important areas)
-
+
+
+ setIsHoveringGenerateZoomButton(true)}
+ onMouseLeave={() => setIsHoveringGenerateZoomButton(false)}
+ onMouseDown={(e) => e.stopPropagation()}
+ >
+
+
+
+
}
>
@@ -622,12 +676,14 @@ export function ZoomTrack(props: {
{(details) => (
diff --git a/apps/desktop/src/routes/editor/color-utils.tsx b/apps/desktop/src/routes/editor/color-utils.tsx
index 010f68b2c4..b273470c77 100644
--- a/apps/desktop/src/routes/editor/color-utils.tsx
+++ b/apps/desktop/src/routes/editor/color-utils.tsx
@@ -41,6 +41,18 @@ export function RgbInput(props: {
let colorInput!: HTMLInputElement;
+ const commitValue = (raw: string) => {
+ const trimmed = raw.trim();
+ const value = hexToRgb(trimmed);
+ if (value) {
+ const [r, g, b] = value;
+ props.onChange([r, g, b]);
+ setText(rgbToHex([r, g, b]));
+ return true;
+ }
+ return false;
+ };
+
return (
diff --git a/apps/desktop/src/routes/editor/text-style.tsx b/apps/desktop/src/routes/editor/text-style.tsx
index e0e3e56b30..e819ae6ce0 100644
--- a/apps/desktop/src/routes/editor/text-style.tsx
+++ b/apps/desktop/src/routes/editor/text-style.tsx
@@ -47,6 +47,17 @@ export function HexColorInput(props: {
let prevColor = props.value;
let colorInput!: HTMLInputElement;
+ const commitValue = (raw: string) => {
+ const trimmed = raw.trim();
+ if (/^#[0-9A-F]{6}$/i.test(trimmed)) {
+ const normalized = `#${trimmed.slice(1).toUpperCase()}`;
+ props.onChange(normalized);
+ setText(normalized);
+ return true;
+ }
+ return false;
+ };
+
return (