Skip to content

feat(screenshot): sensitive and highlight masks with shapes, feather and tint - #3582

Open
AatmanAJ wants to merge 7 commits into
CapSoftware:mainfrom
AatmanAJ:feat/screenshot-mask-studio
Open

AatmanAJ wants to merge 7 commits into
CapSoftware:mainfrom
AatmanAJ:feat/screenshot-mask-studio

Conversation

@AatmanAJ

@AatmanAJ AatmanAJ commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Turns the screenshot editor's Mask tool into a proper mask feature, laid out the same way as the video editor's mask panel.

The mask bar (one row, same pills and sliders as the rest of the annotation bar):

[Sensitive | Highlight] │ mode controls │ [▢ ◯] ⚙

  • Sensitive hides what's inside the mask: Blur · Pixelate · Solid. Blur and Pixelate get an Intensity slider (range now goes up to 80); Solid gets a colour swatch instead.
  • Highlight dims everything outside the mask, like the video editor's Highlight mask, using Darkness and Blur sliders. Blur at 0 is a flat dim (spotlight); above 0 it blurs and dims the outside (focus). Switching to Highlight sets a 60% darkness and a 24px feather, the same way the video editor's kind switch sets its own defaults.
  • Shape: Rectangle or Ellipse icon buttons with tooltips.
  • ⚙ Mask settings opens one small popover built exactly like the header popovers (Padding, Rounding and the others: same surface, same slider rows). It holds Feather and Corners (rectangles only), plus Tint with a colour swatch for Sensitive Blur/Pixelate.
  • New masks copy the look of the selected mask, or the last mask drawn.

How it renders

  • A new kernel, cap_gpui_kernels::mask_style::styled_mask, applies the effect (blur, pixelate or solid), then the tint, then the shape. The shape is a signed-distance coverage for a rounded rect or ellipse, with a feather that only fades inward. An inverted mask flips the coverage, and opacity is applied last, into alpha. The overlay is straight alpha, so the preview (GPUI sprite blend) and the export (blit_over) composite the same pixels.
  • An inverted (Highlight) mask resamples the screenshot's content rect, with its own rect cut out as the hole. To know that rect, the export gets it through a new RawFrame::content (computed with the same image_rect the preview uses), and scale_annotations scales feather and corner radius along with maskLevel.
  • MaskRequest carries a hashable MaskStyle plus the shape rect, so the overlay cache still reuses, cancels and retires images correctly when any style field changes.
  • With default options the kernel skips the coverage pass, so existing masks render byte-for-byte as before (there's a test for this).

Data model / compatibility

  • New optional Annotation.maskOptions (shape, cornerRadius, feather, invert, solid, color, tint). Every field has a default, and it is skip_serializing_if = None, so existing projects are unchanged on disk.
  • Older builds ignore maskOptions and still see a valid maskType. A Solid redaction falls back to a blur rather than exposing what's under it, and a corrupt (NaN) opacity falls back to opaque.
  • No new MaskType variants.

i18n: eight new strings, translated in all seven catalogs, and added to the literal surface test. screenshot_editor.annotation.mask_style was removed because nothing reads it any more. The Sensitive/Highlight labels reuse the video editor's existing strings.

Test plan

  • cargo test --bin cap-gpui: 1572 passed
  • cargo test -p cap-gpui-kernels: 82 lib tests passed, including tests for the mask kernel (byte-identical legacy output, solid fill, tint, ellipse, rounded corners, feather falloff, invert hole, opacity, non-finite fallbacks)
  • cargo test -p cap-i18n-build, cargo test -p cap-project (maskOptions serde round-trip)
  • Unit tests for the Sensitive/Highlight switch, Highlight darkness/blur conversion, shape switching and the colour-pick tint
  • Manual: Sensitive Blur, Pixelate and Solid, then Highlight with blur at 0 and above 0. The preview and a saved PNG should match.
  • Manual: Highlight on a padded screenshot with a background. Only the screenshot content should dim.

Linux harness

  • The GNOME "editor export to MP4 and GIF" step failed once here with Cap gone (gif_cap_alive=no). Its log ends in a SIGSEGV while the shared rendering device was being created: the Vulkan loader had just logged Removing driver libvulkan_radeon.so due to not having any physical devices, the point where it destroys that driver's instance and dlcloses its library, while the main thread was drawing with lavapipe. The editor had opened a video recording, so no mask code ran.

  • run-desktop.sh now points the loader at the lavapipe manifest alone on the vkms images (VK_DRIVER_FILES), next to the existing device_select workaround, so the six hardware drivers that never find a device there are not loaded and unloaded under Cap. A real GPU handed in through DOCKER_EXTRA --gpus keeps the full list, and a caller that sets VK_DRIVER_FILES or VK_ICD_FILENAMES itself (the Hyprland OpenGL step hides every driver that way) is left alone, since the loader prefers VK_DRIVER_FILES. The README's harness-only workarounds note covers it.

  • The "Hyprland screenshot" step has failed on every PR since 2026-10-29 (screenshot_size=1024x768): Arch now ships Hyprland 0.57, which removed its hyprlang config support and reads hyprland.lua alone, so the harness's hyprland.conf (1920x1080 monitor, logo off, stdout logs, waybar autostart) was ignored and a default config generated. arch-up.sh now writes the same settings as hyprland.lua.

Masks gain shapes (rectangle, rounded, ellipse), feathered edges, an
invert mode that hides everything outside the area, colour tint, a Solid
redaction style, opacity, and seven one-click presets.

The look is baked into each overlay by a new kernel (mask_style), so the
preview and the export composite the same pixels. Options live in an
optional maskOptions field that older builds ignore; Solid keeps a
hiding maskType alongside so a redaction never degrades to nothing.
@vercel

vercel Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Someone is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

Comment on lines +602 to +607
fn mask_anchor(&self, panel: MaskPanel) -> BoundsCell {
self.annotation_state
.mask_anchors
.get(&panel)
.cloned()
.unwrap_or_default()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Popovers lose their button positions

Both mask popovers always open at the fallback position. Nothing inserts a cell into mask_anchors, so mask_anchor() returns a fresh cell each time. The button writes its bounds into a cell that the popover never reads. Initialize and reuse one cell per panel so each popover opens under its button.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/screenshot_annotations/mask.rs
Line: 602-607

Comment:
**Popovers lose their button positions**

Both mask popovers always open at the fallback position. Nothing inserts a cell into `mask_anchors`, so `mask_anchor()` returns a fresh cell each time. The button writes its bounds into a cell that the popover never reads. Initialize and reuse one cell per panel so each popover opens under its button.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, valid. Fixed in 4f20ec7: both panel anchors are now seeded once in init_annotation_inputs, so the button writes into the same cell the popover reads.

Comment on lines 2773 to 2780
ColorTarget::Mask => {
let options = annotation.mask_options.get_or_insert_with(Default::default);
Self::ensure_mask_tint_visible(options);
&mut options.color
}
};
if *field == value {
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Same-color picks skip tint updates

Picking the current color on an untinted blur or pixelate mask changes tint to 0.4, then returns false because the color matches. Picking black on a default mask takes this path. edit_project() then skips the preview refresh, save, and history entry. Count the tint change when deciding whether the edit changed anything.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/screenshot_annotations.rs
Line: 2773-2780

Comment:
**Same-color picks skip tint updates**

Picking the current color on an untinted blur or pixelate mask changes `tint` to `0.4`, then returns `false` because the color matches. Picking black on a default mask takes this path. `edit_project()` then skips the preview refresh, save, and history entry. Count the tint change when deciding whether the edit changed anything.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 4f20ec7: ensure_mask_tint_visible now returns whether it changed the tint, and the colour edit reports a change when either the colour or the tint moved. Added a unit test.

Comment on lines +541 to +542
/// A new mask copies the look of the selected mask, or else the last one
/// drawn, so a run of redactions does not need restyling one by one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Comment breaks repository rules

The new comment above mask_style_source() only describes the feature's behavior. AGENTS.md requires comments to capture non-obvious context and explicitly bans comments that merely describe the change being made. Remove it or replace it with a necessary explanation of a non-obvious constraint. This repository requirement must be satisfied before merging.

Suggested change
/// A new mask copies the look of the selected mask, or else the last one
/// drawn, so a run of redactions does not need restyling one by one.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/screenshot_annotations/mask.rs
Line: 541-542

Comment:
**Comment breaks repository rules**

The new comment above `mask_style_source()` only describes the feature's behavior. `AGENTS.md` requires comments to capture non-obvious context and explicitly bans comments that merely describe the change being made. Remove it or replace it with a necessary explanation of a non-obvious constraint. This repository requirement must be satisfied before merging.

```suggestion

```

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Removed in 4f20ec7, along with a few other comments in the new mask code that only narrated behaviour.

@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review — all three findings are addressed in 4f20ec7.

…ask model

Masks are Sensitive (blur, pixelate or solid) or Highlight (dim and
optionally blur everything outside), the same split the video editor's
mask panel uses. Shape sits in the bar; feather, corners and tint move
into one settings popover that reuses the header popovers' chrome. The
presets list, invert pill and Adjust panel are gone.
@AatmanAJ AatmanAJ changed the title feat(screenshot): mask studio – presets, shapes, feather, invert, tint and solid redaction feat(screenshot): sensitive and highlight masks with shapes, feather and tint Oct 29, 2026
Aatman Jain added 4 commits October 29, 2026 23:57
…images

The GNOME export run on this branch lost Cap to a SIGSEGV while the shared
rendering device was being created: the loader had just logged "Removing
driver libvulkan_radeon.so due to not having any physical devices", which is
where it destroys that driver's instance and dlcloses its library, while the
main thread was still drawing with lavapipe. The hardware drivers never find a
device on vkms/vgem, so VK_DRIVER_FILES now names the lavapipe manifest alone,
next to the existing device_select workaround. A real GPU passed in through
DOCKER_EXTRA keeps the full driver list.
The Hyprland OpenGL step hides every Vulkan driver with
VK_ICD_FILENAMES=/nonexistent.json to put Cap on wgpu's GL backend. The loader
prefers VK_DRIVER_FILES over that older variable, so the lavapipe pin brought
the driver back and the step's "(Gl)" adapter check failed. The pin now stays
off whenever the caller passes VK_DRIVER_FILES or VK_ICD_FILENAMES itself.
Arch now ships Hyprland 0.57 (2026-10-28), which removed its hyprlang config
support and reads hyprland.lua alone. The harness's hyprland.conf was ignored:
Hyprland logged "No config file found; attempting to generate", left the vkms
output at its 1024x768 default and stopped logging to stdout, so every
"Hyprland screenshot" step since 2026-10-29 failed on screenshot_size. The same
monitor, misc, debug and autostart settings now go into hyprland.lua, with
stdout logging turned on so compositor.log keeps collecting them.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant