feat: add freehand draw tool to screenshot editor - #2136
Conversation
I needed the ability to quickly annotate and mark up screenshots with freehand strokes — circling things, underlining, scribbling notes — without reaching for an external tool. The existing shape tools (arrow, rectangle, circle) are great for precise callouts but too rigid for quick markup. This adds a Draw tool (pencil icon, keyboard shortcut D) to the screenshot editor toolbar, sitting between Select and Arrow. It records freehand strokes as normalized point data so the drawing scales correctly with resize and preserves its shape across edits. - New `draw` variant in AnnotationType (Rust + TS) - `points` field on Annotation struct for path data - Smooth quadratic bezier rendering (SVG path + canvas export) - Minimum distance filter to keep point arrays reasonable - Works with existing stroke color, width, and opacity controls Co-authored-by: Cursor <cursoragent@cursor.com>
| d={smoothPathFromPoints(props.annotation.points.map((p) => [ | ||
| props.annotation.x + p[0] * (props.annotation.width || 1), | ||
| props.annotation.y + p[1] * (props.annotation.height || 1), | ||
| ] as [number, number]))} |
There was a problem hiding this comment.
Signed dimensions mirror draw paths
When a west or north resize handle is dragged past the opposite edge, the shared resize logic produces a negative dimension and this mapping applies it directly to the normalized points, causing the stroke to be mirrored or displaced relative to its selection box; the export path reproduces the same incorrect geometry.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/screenshot-editor/AnnotationLayer.tsx
Line: 872-875
Comment:
**Signed dimensions mirror draw paths**
When a west or north resize handle is dragged past the opposite edge, the shared resize logic produces a negative dimension and this mapping applies it directly to the normalized points, causing the stroke to be mirrored or displaced relative to its selection box; the export path reproduces the same incorrect geometry.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
This is intentional. Mirroring the stroke when dragged past the opposite edge is consistent with how rectangles and circles behave. The negative dimension flips the rendering, which is the expected UX when you "flip" an annotation by crossing its bounds.
Users can redrag it up in order to revert it to the "Correct" render.
Flip.mp4
There was a problem hiding this comment.
Got it — thanks for the video. Mirroring on negative dimensions being consistent with the existing shape tools is a good reason to keep it as-is, and the ability to revert by redragging makes it recoverable. Withdrawing the concern.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
| export type Annotation = { id: string; type: AnnotationType; x: number; y: number; width: number; height: number; strokeColor: string; strokeWidth: number; fillColor: string; opacity: number; rotation: number; text: string | null; maskType?: MaskType | null; maskLevel?: number | null; points?: ([number, number])[] | null } | ||
| export type AnnotationType = "arrow" | "circle" | "rectangle" | "text" | "mask" | "draw" |
There was a problem hiding this comment.
Generated binding edited manually
This generated Tauri binding was changed by hand instead of through the repository's Specta generation process, so a routine debug run or binding-generation test can replace the committed output and create generated-file drift.
Context Used: AGENTS.md (source)
Knowledge Base Used: Desktop Frontend (apps/desktop/src)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/tauri.ts
Line: 572-573
Comment:
**Generated binding edited manually**
This generated Tauri binding was changed by hand instead of through the repository's Specta generation process, so a routine debug run or binding-generation test can replace the committed output and create generated-file drift.
**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))
**Knowledge Base Used:** [Desktop Frontend (apps/desktop/src)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/desktop-frontend.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Re: the two review comments from greptile-apps: P1 (signed dimensions mirror draw paths): This is intentional. Mirroring the stroke when dragged past the opposite edge is consistent with how rectangles and circles behave — the negative dimension flips the rendering, which is the expected UX when you "flip" an annotation by crossing its bounds. P2 (generated binding edited manually): Acknowledged in the PR description. The manual edit to |
When a draw stroke is resized past the opposite edge, persist the normalized bounding box and flipped points on mouse-up so the next handle drag starts from the new visual origin. Co-authored-by: Cursor <cursoragent@cursor.com>
Regenerated desktop TypeScript bindings via export_typescript_bindings; specta output already matches the committed tauri.ts types. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Follow-up on the generated-bindings comment: I regenerated The Rust |
Each mouse-up still commits a separate annotation, but Draw stays selected so you can keep sketching without clicking Done between strokes. New strokes inherit the last stroke color, width, and opacity. Co-authored-by: Cursor <cursoragent@cursor.com>
The layers list crashed on mouse-up because it had no icon or label for draw annotations. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep Draw focused on sketching. Bounding-box handles only appear after switching to the Select tool. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Follow-up on the draw-tool UX after trying it in the editor: Multiple strokes in one session. Draw used to jump back to Select on mouse-up, so a smiley face (two eyes + a mouth) meant Done + reselect Draw between every stroke. It now stays on Draw. Each click-drag-release is still its own annotation, and the next stroke inherits the last color / width / opacity. Layers crash. Finishing a stroke blew up the layers panel because it had no icon or label for No transform handles while drawing. Bounding-box handles were showing as soon as a stroke landed, which got in the way of sketching. Handles only appear after you switch to Select ( Multi-Draw.mp4 |
Summary
I needed the ability to quickly annotate and mark up screenshots with freehand strokes — circling things, underlining, scribbling notes — without reaching for an external tool. The existing shape tools (arrow, rectangle, circle) are great for precise callouts but too rigid for quick markup.
This adds a Draw tool (pencil icon, keyboard shortcut
D) to the screenshot editor toolbar between Select and Arrow. It records freehand strokes as normalized point data so the drawing scales correctly with resize and preserves its shape across edits.Changes
drawvariant inAnnotationType(Rust + TS)pointsfield onAnnotationstruct for storing path dataDto activateNotes
pointsfield uses#[serde(default)]so existing projects without it deserialize finetauri.tswas updated manually to match the Rust change — will need regeneration on next specta runExample video
Cap-Screenshot-Draw-Feature.mp4
Test plan