Skip to content

Bounty: Use SVG's for common cursors (Custom cursor functionality) #630 #722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f604e61
Bounty: Use SVG's for common cursors (Custom cursor functionality) #630
NalinDalal Jul 4, 2025
e278321
Merge branch 'CapSoftware:main' into main
NalinDalal Jul 5, 2025
995140d
Merge branch 'CapSoftware:main' into main
NalinDalal Jul 9, 2025
8b63247
Merge branch 'main' into pr/722
richiemcilroy Jul 9, 2025
1313428
check for rust
NalinDalal Jul 10, 2025
463ab4b
Merge remote-tracking branch 'origin' into NalinDalal/main
oscartbeaumont Jul 29, 2025
e052723
fix
oscartbeaumont Jul 29, 2025
a3fd04f
move cursors into rendering + drop `svg_filename` and add `Cursor::load`
oscartbeaumont Jul 29, 2025
e94955e
reset changes
oscartbeaumont Jul 29, 2025
fde4395
remove unused stuff + upgrade resvg
oscartbeaumont Jul 29, 2025
50af9d8
overhaul
oscartbeaumont Jul 29, 2025
0a71e22
ensure prepare svg system is working + bundle included cursors
oscartbeaumont Jul 29, 2025
dc2d447
svg output scaling
oscartbeaumont Jul 29, 2025
bde57fc
Merge branch 'CapSoftware:main' into main
NalinDalal Jul 30, 2025
9035c3e
`cap-cursor-info` work + cursor enums populated
oscartbeaumont Jul 30, 2025
8906225
windows work
oscartbeaumont Jul 30, 2025
8f37631
Merge remote-tracking branch 'origin' into NalinDalal/main
oscartbeaumont Jul 30, 2025
14b4c03
fix `CursorShape` serialize impl
oscartbeaumont Jul 30, 2025
add5389
invalidate cache when toggling scg assets + better macOS assets
oscartbeaumont Jul 30, 2025
e7729b9
Merge remote-tracking branch 'origin' into NalinDalal/main
oscartbeaumont Jul 30, 2025
be3f106
bruh macos `NSCursor` is cringe
oscartbeaumont Jul 30, 2025
4238502
Cursor debug CLI wip
oscartbeaumont Jul 30, 2025
1a32554
move to hash-based `NSCursor` comparision
oscartbeaumont Jul 30, 2025
3186a83
position svg correctly and scale up a bit more
oscartbeaumont Jul 30, 2025
b18edad
windows support for CLI
oscartbeaumont Jul 30, 2025
a4923a2
wip
oscartbeaumont Jul 30, 2025
3211ae2
windows hashing wip
oscartbeaumont Jul 30, 2025
4812e79
windows cli + get hashes
oscartbeaumont Jul 30, 2025
09558ee
macos hashes
oscartbeaumont Jul 30, 2025
bf14b44
disable Windows cursors without assets
oscartbeaumont Jul 30, 2025
c86e439
hotspot viewer
oscartbeaumont Jul 30, 2025
4decc31
windows cursor hotspots + cursor viewer site
oscartbeaumont Jul 30, 2025
f9dbb91
set cursor info deps as dev
oscartbeaumont Jul 30, 2025
27b8c19
nit
oscartbeaumont Jul 30, 2025
011f873
format
oscartbeaumont Jul 30, 2025
ff4491a
Merge branch 'CapSoftware:main' into main
NalinDalal Jul 30, 2025
7ded393
Merge branch 'main' into main
NalinDalal Jul 31, 2025
df68315
wip
oscartbeaumont Jul 31, 2025
e21550b
Merge remote-tracking branch 'origin' into NalinDalal/main
oscartbeaumont Aug 4, 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
444 changes: 343 additions & 101 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Resul

use cap_project::*;
RecordingMeta {
platform: Some(Platform::default()),
platform: Platform::default(),
project_path: recording_dir.clone(),
sharing: None,
pretty_name: screenshot_name,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ async fn handle_recording_finish(
};

let meta = RecordingMeta {
platform: Some(Platform::default()),
platform: Platform::default(),
project_path: recording_dir.clone(),
sharing,
pretty_name: format!(
Expand Down
84 changes: 48 additions & 36 deletions apps/desktop/src/routes/editor/ConfigSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import { createStore, produce } from "solid-js/store";
import { Dynamic } from "solid-js/web";
import toast from "solid-toast";

import IconLucideSparkles from "~icons/lucide/sparkles";
import colorBg from "~/assets/illustrations/color.webp";
import gradientBg from "~/assets/illustrations/gradient.webp";
import imageBg from "~/assets/illustrations/image.webp";
Expand Down Expand Up @@ -241,7 +241,7 @@ export function ConfigSidebar() {
id: TAB_IDS.camera,
icon: IconCapCamera,
disabled: editorInstance.recordings.segments.every(
(s) => s.camera === null
(s) => s.camera === null,
),
},
{ id: TAB_IDS.audio, icon: IconCapAudioOn },
Expand Down Expand Up @@ -275,7 +275,7 @@ export function ConfigSidebar() {
class={cx(
"flex justify-center relative border-transparent border z-10 items-center rounded-md size-9 transition will-change-transform",
state.selectedTab !== item.id &&
"group-hover:border-gray-300 group-disabled:border-none"
"group-hover:border-gray-300 group-disabled:border-none",
)}
>
<Dynamic component={item.icon} />
Expand Down Expand Up @@ -313,7 +313,7 @@ export function ConfigSidebar() {
optionValue="value"
optionTextValue="name"
value={STEREO_MODES.find(
(v) => v.value === project.audio.micStereoMode
(v) => v.value === project.audio.micStereoMode,
)}
onChange={(v) => {
if (v) setProject("audio", "micStereoMode", v.value);
Expand Down Expand Up @@ -480,6 +480,18 @@ export function ConfigSidebar() {
</div>
</KCollapsible.Content>
</KCollapsible>
<Field
name="High Quality SVG Cursors"
icon={<IconLucideSparkles />}
value={
<Toggle
checked={(project.cursor as any).useSvg ?? true}
onChange={(value) => {
setProject("cursor", "useSvg" as any, value);
}}
/>
}
/>
</Show>

{/* <Field name="Motion Blur">
Expand Down Expand Up @@ -660,7 +672,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {

// Get the raw path without any URL prefixes
const rawPath = decodeURIComponent(
photoUrl.replace("file://", "")
photoUrl.replace("file://", ""),
);

debouncedSetProject(rawPath);
Expand Down Expand Up @@ -720,7 +732,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
}
},
},
{ passive: false }
{ passive: false },
);

let fileInput!: HTMLInputElement;
Expand Down Expand Up @@ -886,7 +898,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
project.background.source.path
) {
const convertedPath = convertFileSrc(
project.background.source.path
project.background.source.path,
);
// Only use converted path if it's valid
if (convertedPath) {
Expand All @@ -904,7 +916,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
project.background.source as {
path?: string;
}
).path?.includes(w.id)
).path?.includes(w.id),
);
// Only use wallpaper URL if it exists
if (selectedWallpaper?.url) {
Expand Down Expand Up @@ -971,7 +983,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
<KTabs.Trigger
onClick={() =>
setBackgroundTab(
key as keyof typeof BACKGROUND_THEMES
key as keyof typeof BACKGROUND_THEMES,
)
}
value={key}
Expand All @@ -988,17 +1000,17 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
<KRadioGroup
value={
project.background.source.type === "wallpaper"
? wallpapers()?.find((w) =>
? (wallpapers()?.find((w) =>
(
project.background.source as { path?: string }
).path?.includes(w.id)
)?.url ?? undefined
).path?.includes(w.id),
)?.url ?? undefined)
: undefined
}
onChange={(photoUrl) => {
try {
const wallpaper = wallpapers()?.find(
(w) => w.url === photoUrl
(w) => w.url === photoUrl,
);
if (!wallpaper) return;

Expand Down Expand Up @@ -1199,7 +1211,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
setProject(
"background",
"source",
backgrounds.color
backgrounds.color,
);
}
}}
Expand Down Expand Up @@ -1283,7 +1295,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
const rawNewAngle =
Math.round(
start +
(downEvent.clientY - moveEvent.clientY)
(downEvent.clientY - moveEvent.clientY),
) % max;
const newAngle = moveEvent.shiftKey
? rawNewAngle
Expand All @@ -1298,7 +1310,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
) {
commands.performHapticFeedback(
"Alignment",
"Now"
"Now",
);
}

Expand All @@ -1308,7 +1320,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
newAngle < 0 ? newAngle + max : newAngle,
});
},
})
}),
);
}}
>
Expand All @@ -1333,7 +1345,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
setProject(
"background",
"source",
backgrounds.gradient
backgrounds.gradient,
);
}
}}
Expand All @@ -1342,7 +1354,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
class="rounded-lg transition-all duration-200 cursor-pointer size-8 peer-checked:hover:opacity-100 peer-hover:opacity-70 peer-checked:ring-2 peer-checked:ring-gray-500 peer-checked:ring-offset-2 peer-checked:ring-offset-gray-200"
style={{
background: `linear-gradient(${angle()}deg, rgb(${gradient.from.join(
","
",",
)}), rgb(${gradient.to.join(",")}))`,
}}
/>
Expand Down Expand Up @@ -1506,9 +1518,9 @@ function CameraConfig(props: { scrollRef: HTMLDivElement }) {
item.x === "left"
? "left-2"
: item.x === "right"
? "right-2"
: "left-1/2 transform -translate-x-1/2",
item.y === "top" ? "top-2" : "bottom-2"
? "right-2"
: "left-1/2 transform -translate-x-1/2",
item.y === "top" ? "top-2" : "bottom-2",
)}
onClick={() => setProject("camera", "position", item)}
>
Expand Down Expand Up @@ -1537,7 +1549,7 @@ function CameraConfig(props: { scrollRef: HTMLDivElement }) {
optionValue="value"
optionTextValue="name"
value={CAMERA_SHAPES.find(
(v) => v.value === project.camera.shape
(v) => v.value === project.camera.shape,
)}
onChange={(v) => {
if (v) setProject("camera", "shape", v.value);
Expand Down Expand Up @@ -1744,7 +1756,7 @@ function ZoomSegmentConfig(props: {
"zoomSegments",
props.segmentIndex,
"amount",
v[0]
v[0],
)
}
minValue={1}
Expand All @@ -1763,7 +1775,7 @@ function ZoomSegmentConfig(props: {
"zoomSegments",
props.segmentIndex,
"mode",
v === "auto" ? "auto" : { manual: states.manual }
v === "auto" ? "auto" : { manual: states.manual },
);
}}
>
Expand Down Expand Up @@ -1804,7 +1816,7 @@ function ZoomSegmentConfig(props: {

const st = start();
let i = project.timeline?.segments.findIndex(
(s) => s.start <= st && s.end > st
(s) => s.start <= st && s.end > st,
);
if (i === undefined || i === -1) return 0;
return i;
Expand All @@ -1816,7 +1828,7 @@ function ZoomSegmentConfig(props: {
// TODO: this shouldn't be so hardcoded
`${
editorInstance.path
}/content/segments/segment-${segmentIndex()}/display.mp4`
}/content/segments/segment-${segmentIndex()}/display.mp4`,
);
});

Expand All @@ -1834,8 +1846,8 @@ function ZoomSegmentConfig(props: {
},
() => {
render();
}
)
},
),
);

const render = () => {
Expand All @@ -1850,7 +1862,7 @@ function ZoomSegmentConfig(props: {
0,
0,
canvasRef.width!,
canvasRef.height!
canvasRef.height!,
);
};

Expand Down Expand Up @@ -1913,19 +1925,19 @@ function ZoomSegmentConfig(props: {
Math.min(
(moveEvent.clientX - bounds.left) /
bounds.width,
1
1,
),
0
0,
),
y: Math.max(
Math.min(
(moveEvent.clientY - bounds.top) /
bounds.height,
1
1,
),
0
0,
),
}
},
);
},
});
Expand Down Expand Up @@ -1989,7 +2001,7 @@ function ClipSegmentConfig(props: {
disabled={
(
project.timeline?.segments.filter(
(s) => s.recordingSegment === props.segment.recordingSegment
(s) => s.recordingSegment === props.segment.recordingSegment,
) ?? []
).length < 2
}
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,8 @@ export type CurrentRecording = { target: CurrentRecordingTarget; type: Recording
export type CurrentRecordingChanged = null
export type CurrentRecordingTarget = { window: { id: number; bounds: Bounds } } | { screen: { id: number } } | { area: { screen: number; bounds: Bounds } }
export type CursorAnimationStyle = "regular" | "slow" | "fast"
export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number }
export type CursorMeta = { imagePath: string; hotspot: XY<number> }
export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number; useSvg?: boolean }
export type CursorMeta = { imagePath: string; hotspot: XY<number>; hash?: string | null }
export type CursorType = "pointer" | "circle"
export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta }
export type DownloadProgress = { progress: number; message: string }
Expand Down Expand Up @@ -370,8 +370,8 @@ export type Preset = { name: string; config: ProjectConfiguration }
export type PresetsStore = { presets: Preset[]; default: number | null }
export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: Camera; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null; captions?: CaptionsData | null }
export type ProjectRecordingsMeta = { segments: SegmentRecordings[] }
export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform: Platform | null; pretty_name: string; sharing?: SharingMeta | null }
export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform: Platform | null; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType }
export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform; pretty_name: string; sharing?: SharingMeta | null }
export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType }
export type RecordingMode = "studio" | "instant"
export type RecordingOptionsChanged = null
export type RecordingStarted = null
Expand Down
23 changes: 23 additions & 0 deletions crates/cursor-info/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "cap-cursor-info"
version = "0.0.0"
edition = "2024"
publish = false

[dev-dependencies]
hex = "0.4.3"
image = "0.25.6"
sha2 = "0.10.9"

[target.'cfg(target_os = "macos")'.dev-dependencies]
objc2-app-kit = "0.3.1"
objc2 = "0.6.1"

[target.'cfg(target_os= "windows")'.dev-dependencies]
windows = { workspace = true, features = [
"Win32_Graphics_Gdi",
"Win32_UI_WindowsAndMessaging",
] }

[lints]
workspace = true
3 changes: 3 additions & 0 deletions crates/cursor-info/assets/mac/LICENCE
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This software is released under the Apple User Agreement.

Icons taken from: https://github.com/daviddarnes/mac-cursors
1 change: 1 addition & 0 deletions crates/cursor-info/assets/mac/arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading