From 477d1b1c4e667aa39b416b5a83391cd236c3247c Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Fri, 8 May 2026 12:21:02 +0800 Subject: [PATCH] {"schema":"maestro/commit/1","summary":"Fix screenshot frozen-entry crash and prepare v0.1.6","authority":"manual"} --- Cargo.lock | 8 +- Cargo.toml | 8 +- README.md | 6 +- .../smoke-perf-validation-surface.md | 2 +- docs/runbook/performance-validation.md | 4 +- docs/runbook/scroll-capture-benchmarks.md | 2 +- docs/runbook/validate-release.md | 6 +- docs/spec/capture-session.md | 2 +- .../RsnapNativeHostKit/NativeHostApp.swift | 125 ++++++++++++------ scripts/build_and_run.sh | 2 +- 10 files changed, 105 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f7637d9..6406c73f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3363,7 +3363,7 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rsnap" -version = "0.1.5" +version = "0.1.6" dependencies = [ "color-eyre", "directories", @@ -3376,7 +3376,7 @@ dependencies = [ [[package]] name = "rsnap-capture-core" -version = "0.1.5" +version = "0.1.6" dependencies = [ "image", "serde", @@ -3384,7 +3384,7 @@ dependencies = [ [[package]] name = "rsnap-host-ffi" -version = "0.1.5" +version = "0.1.6" dependencies = [ "rsnap-capture-core", "rsnap-overlay", @@ -3392,7 +3392,7 @@ dependencies = [ [[package]] name = "rsnap-overlay" -version = "0.1.5" +version = "0.1.6" dependencies = [ "block2 0.6.2", "color-eyre", diff --git a/Cargo.toml b/Cargo.toml index 2f3a5301..f818adaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ homepage = "https://hack.ink/rsnap" license = "GPL-3.0" readme = "README.md" repository = "https://github.com/hack-ink/rsnap" -version = "0.1.5" +version = "0.1.6" [workspace.dependencies] arboard = { version = "3.6" } @@ -52,9 +52,9 @@ wgpu = { version = "29.0" } winit = { version = "0.30", features = ["rwh_06"] } xcap = { version = "0.9" } -rsnap-capture-core = { version = "0.1.5", path = "packages/rsnap-capture-core" } -rsnap-host-ffi = { version = "0.1.5", path = "packages/rsnap-host-ffi" } -rsnap-overlay = { version = "0.1.5", path = "packages/rsnap-overlay" } +rsnap-capture-core = { version = "0.1.6", path = "packages/rsnap-capture-core" } +rsnap-host-ffi = { version = "0.1.6", path = "packages/rsnap-host-ffi" } +rsnap-overlay = { version = "0.1.6", path = "packages/rsnap-overlay" } [profile.final-release] inherits = "release" diff --git a/README.md b/README.md index 181f0f8c..92ffb12f 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Prototype / in active development. - Menubar and Dock are not included in live window-outline targeting. - Windows support is planned (minimum Windows 10), but not implemented yet. - The scroll-capture engine, deterministic replay, and benchmark surfaces remain in the repository, - but the v0.1.5 native-host release does not expose scroll capture in the toolbar. + but the v0.1.6 native-host release does not expose scroll capture in the toolbar. ## Usage @@ -123,7 +123,7 @@ Rsnap currently relies on **Screen Recording** permission to capture other apps/ - ScreenCaptureKit live sampling on macOS requires macOS 12.3+ and Screen Recording permission. - Normal region/window/monitor capture does not require Accessibility or Input Monitoring. - The retained scroll-capture path uses Screen Recording-backed screenshots plus forwarded wheel - input, but the v0.1.5 native-host release does not expose scroll capture in the toolbar. + input, but the v0.1.6 native-host release does not expose scroll capture in the toolbar. - macOS may describe Screen Recording as `Screen & System Audio Recording` or as direct screen/audio access when Rsnap bypasses the system picker. - Settings -> Permissions shows Screen Recording as the only required permission. - Normal native capture depends on Screen Recording; if access is missing, Rsnap opens the Screen Recording page in System Settings and shows a floating drag-to-grant guide. @@ -156,7 +156,7 @@ Rsnap currently relies on **Screen Recording** permission to capture other apps/ ### Current scroll-capture status -Scroll capture is temporarily hidden in the v0.1.5 native-host release. The retained Rust +Scroll capture is temporarily hidden in the v0.1.6 native-host release. The retained Rust scroll-capture session, deterministic replay, and benchmark surfaces remain for validation and future re-enablement, but users should not expect a `Scroll Capture` toolbar item in this release. diff --git a/docs/reference/smoke-perf-validation-surface.md b/docs/reference/smoke-perf-validation-surface.md index 058f8f81..dec18a5c 100644 --- a/docs/reference/smoke-perf-validation-surface.md +++ b/docs/reference/smoke-perf-validation-surface.md @@ -17,7 +17,7 @@ Depends on: `docs/runbook/performance-validation.md`; `docs/spec/performance.md` Covers: The current layer map for smoke/perf entrypoints, deterministic replay/bench surfaces, overlay runtime integration tests, and scroll-capture session semantics tests. -Release exposure note: v0.1.5 hides user-facing scroll capture in the native host. The +Release exposure note: v0.1.6 hides user-facing scroll capture in the native host. The scroll-capture entries in this reference describe retained internal validation assets, not a visible toolbar feature in that release. diff --git a/docs/runbook/performance-validation.md b/docs/runbook/performance-validation.md index bfbae8c0..ea207072 100644 --- a/docs/runbook/performance-validation.md +++ b/docs/runbook/performance-validation.md @@ -17,9 +17,9 @@ Depends on: `docs/spec/performance.md` Outputs: A clear command choice for the regression class you are testing, plus a repeatable local baseline workflow for the committed Criterion benchmark targets. -Current release status: v0.1.5 hides user-facing scroll capture in the native host. The replay and +Current release status: v0.1.6 hides user-facing scroll capture in the native host. The replay and benchmark commands in this runbook still own retained internal scroll-capture engine validation and -future re-enablement work, but they are not evidence that the v0.1.5 toolbar exposes scroll +future re-enablement work, but they are not evidence that the v0.1.6 toolbar exposes scroll capture. ## Command selection diff --git a/docs/runbook/scroll-capture-benchmarks.md b/docs/runbook/scroll-capture-benchmarks.md index ad2d577c..4473f0d0 100644 --- a/docs/runbook/scroll-capture-benchmarks.md +++ b/docs/runbook/scroll-capture-benchmarks.md @@ -14,7 +14,7 @@ Depends on: `docs/spec/performance.md` Outputs: A repeatable local benchmark run, an optional saved Criterion baseline, and a clear understanding of what the synthetic fixture is intended to cover. -Current release status: v0.1.5 hides user-facing scroll capture in the native host. This runbook +Current release status: v0.1.6 hides user-facing scroll capture in the native host. This runbook still applies to the retained internal scroll-capture engine, replay, and future re-enablement work. diff --git a/docs/runbook/validate-release.md b/docs/runbook/validate-release.md index c87f084d..0dba80c9 100644 --- a/docs/runbook/validate-release.md +++ b/docs/runbook/validate-release.md @@ -29,7 +29,7 @@ manual first-run/user-flow validation. - Sparkle update signing is configured: `SUPublicEDKey` is checked into `scripts/build_and_run.sh`, and `SPARKLE_PRIVATE_ED_KEY` is available to the Release workflow for signing the published update archive. - - Apple notary credentials are optional for v0.1.5; when absent, the Release workflow still + - Apple notary credentials are optional for v0.1.6; when absent, the Release workflow still publishes a signed but unnotarized macOS zip. 3. Confirm local gates: - `cargo make checks` @@ -58,7 +58,7 @@ Validate these user-visible flows: fullscreen fallback. - Frozen toolbar tools: pointer, pen, arrow, text, mosaic, spotlight, undo, redo, auto-center, Recognize Text, copy, and save. -- Scroll capture is hidden in the v0.1.5 native-host release: the toolbar must not show a scroll +- Scroll capture is hidden in the v0.1.6 native-host release: the toolbar must not show a scroll capture item, and pressing `s` must not enter scroll capture. - Light and dark appearance; Classic Glass and Liquid Glass where the OS and current build support Liquid Glass. @@ -96,7 +96,7 @@ user-entered annotation text. 4. Treat notarization failure as a release blocker only when notary credentials are configured. 5. The Release workflow publishes the signed macOS zip and `appcast.xml` to the GitHub release. It notarizes and staples the app only when notary credentials are configured. It does not - publish crates.io packages or non-macOS desktop archives for v0.1.5. + publish crates.io packages or non-macOS desktop archives for v0.1.6. ## Published Artifact Check diff --git a/docs/spec/capture-session.md b/docs/spec/capture-session.md index 3d9f3152..51018b2f 100644 --- a/docs/spec/capture-session.md +++ b/docs/spec/capture-session.md @@ -162,7 +162,7 @@ product level rather than binding itself to a particular window toolkit or shell ## Scroll capture -- The v0.1.5 native-host release does not expose scroll capture. The frozen toolbar MUST NOT show a +- The v0.1.6 native-host release does not expose scroll capture. The frozen toolbar MUST NOT show a scroll-capture item while the native-host scroll-capture gate is disabled, and plain `s` MUST NOT enter scroll capture in that state. - When scroll capture is re-enabled, it is available only from a dragged-region freeze on macOS. diff --git a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift index 632eaf48..5c17a693 100644 --- a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift +++ b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift @@ -4782,7 +4782,7 @@ final class CaptureHostView: NSView { clearLivePrimaryInteractionState(rendersImmediately: false) resetLivePointerPreview() liveHighlightedWindowPreview = nil - refreshHoveredToolbarAction() + clearHoveredToolbarAction() syncVisibleCursor() needsDisplay = true controller?.updateLivePreviewDemand( @@ -6144,9 +6144,13 @@ final class CaptureHostView: NSView { return nil } - let styleKind = - items.first(where: { $0.selected }) - .flatMap { FrozenAnnotationStyleToolbarKind(selectedTool: $0.kind) } + var styleKind: FrozenAnnotationStyleToolbarKind? + for item in items where item.selected { + if let kind = FrozenAnnotationStyleToolbarKind(selectedTool: item.kind) { + styleKind = kind + break + } + } let metrics = CaptureChrome.toolbarMetrics() let itemCount = CGFloat(items.count) let primaryContentWidth = @@ -6201,14 +6205,17 @@ final class CaptureHostView: NSView { cursorX += metrics.buttonSize + metrics.itemSpacing } - let styleLayout = styleKind.map { - annotationStyleLayout( - for: $0, + let styleLayout: FrozenAnnotationStyleLayout? + if let styleKind { + styleLayout = annotationStyleLayout( + for: styleKind, in: frame, contentWidth: styleContentWidth, metrics: metrics, toolbarAboveSelection: toolbarAboveSelection ) + } else { + styleLayout = nil } return FrozenToolbarLayout( @@ -6336,8 +6343,9 @@ final class CaptureHostView: NSView { } private func visibleToolbarItems() -> [ToolbarItem] { - scene.toolbarItems.compactMap { item in - var item = item + var items: [ToolbarItem] = [] + for originalItem in scene.toolbarItems { + var item = originalItem switch item.kind { case .pen, .arrow, .mosaic, .spotlight, .text: item.enabled = true @@ -6351,14 +6359,15 @@ final class CaptureHostView: NSView { && !chrome.frozenOverlay.keepsFrozenSelectionFixed case .scroll: guard controller?.scrollCaptureToolbarEnabled == true else { - return nil + continue } item.enabled = controller?.scrollCaptureToolbarEnabled ?? false default: break } - return item + items.append(item) } + return items } private func toolbarItem(_ kind: ToolbarItemKind) -> ToolbarItem? { @@ -6366,29 +6375,11 @@ final class CaptureHostView: NSView { } private func toolbarAction(at point: CGPoint) -> ToolbarItemKind? { - guard scene.mode == .frozen, let selection = localFrozenSelectionRect(), - let layout = toolbarLayout(for: selection) - else { - return nil - } - return layout.items.first(where: { $0.frame.contains(point) && $0.enabled })?.kind + frozenToolbarHitState(at: point).toolbarAction } private func annotationStyleAction(at point: CGPoint) -> FrozenAnnotationStyleAction? { - guard scene.mode == .frozen, let selection = localFrozenSelectionRect(), - let styleLayout = toolbarLayout(for: selection)?.annotationStyle - else { - return nil - } - if styleLayout.decreaseFrame.contains(point) { - return .decreaseSize - } - if styleLayout.increaseFrame.contains(point) { - return .increaseSize - } - return styleLayout.swatches.first(where: { $0.frame.contains(point) }).map { - .color($0.color) - } + frozenToolbarHitState(at: point).annotationStyleAction } private func annotationStyleSizeControlContains(_ point: CGPoint) -> Bool { @@ -6401,12 +6392,7 @@ final class CaptureHostView: NSView { } private func toolbarFrameContains(_ point: CGPoint) -> Bool { - guard scene.mode == .frozen, let selection = localFrozenSelectionRect(), - let layout = toolbarLayout(for: selection) - else { - return false - } - return layout.frame.contains(point) + frozenToolbarHitState(at: point).pointerOverToolbar } private func performToolbarAction(_ action: ToolbarItemKind) { @@ -6426,11 +6412,70 @@ final class CaptureHostView: NSView { controller?.performFrozenAnnotationStyleAction(action) } + private func frozenToolbarHitState(at point: CGPoint) -> ( + pointerOverToolbar: Bool, + toolbarAction: ToolbarItemKind?, + annotationStyleAction: FrozenAnnotationStyleAction? + ) { + guard scene.mode == .frozen, let selection = localFrozenSelectionRect(), + let layout = toolbarLayout(for: selection) + else { + return (false, nil, nil) + } + + var hoveredAction: ToolbarItemKind? + for item in layout.items where item.enabled { + if item.frame.contains(point) { + hoveredAction = item.kind + break + } + } + + var hoveredStyleAction: FrozenAnnotationStyleAction? + if let styleLayout = layout.annotationStyle { + if styleLayout.decreaseFrame.contains(point) { + hoveredStyleAction = .decreaseSize + } else if styleLayout.increaseFrame.contains(point) { + hoveredStyleAction = .increaseSize + } else { + for swatch in styleLayout.swatches where swatch.frame.contains(point) { + hoveredStyleAction = .color(swatch.color) + break + } + } + } + + return (layout.frame.contains(point), hoveredAction, hoveredStyleAction) + } + + private func clearHoveredToolbarAction() { + guard + pointerOverFrozenToolbar || hoveredToolbarAction != nil + || hoveredAnnotationStyleAction != nil + else { + return + } + pointerOverFrozenToolbar = false + hoveredToolbarAction = nil + hoveredAnnotationStyleAction = nil + } + private func refreshHoveredToolbarAction(for localPoint: CGPoint? = nil) { let probePoint = scene.mode == .frozen ? (localPoint ?? currentLocalMousePoint()) : nil - let pointerOverToolbar = probePoint.map(toolbarFrameContains) ?? false - let hoveredAction = probePoint.flatMap(toolbarAction(at:)) - let hoveredStyleAction = probePoint.flatMap(annotationStyleAction(at:)) + let hitState: + ( + pointerOverToolbar: Bool, + toolbarAction: ToolbarItemKind?, + annotationStyleAction: FrozenAnnotationStyleAction? + ) + if let probePoint { + hitState = frozenToolbarHitState(at: probePoint) + } else { + hitState = (false, nil, nil) + } + let pointerOverToolbar = hitState.pointerOverToolbar + let hoveredAction = hitState.toolbarAction + let hoveredStyleAction = hitState.annotationStyleAction if hoveredToolbarAction != hoveredAction || hoveredAnnotationStyleAction != hoveredStyleAction || pointerOverFrozenToolbar != pointerOverToolbar diff --git a/scripts/build_and_run.sh b/scripts/build_and_run.sh index f9db4ae1..452d617e 100755 --- a/scripts/build_and_run.sh +++ b/scripts/build_and_run.sh @@ -65,7 +65,7 @@ APP_VERSION="${RSNAP_NATIVE_HOST_APP_VERSION:-}" if [[ -z "$APP_VERSION" ]]; then APP_VERSION="$(sed -n '/^\[workspace.package\]/,/^\[/s/^version *= *"\(.*\)"/\1/p' "$ROOT_DIR/Cargo.toml" | head -n 1)" fi -APP_VERSION="${APP_VERSION:-0.1.5}" +APP_VERSION="${APP_VERSION:-0.1.6}" require_liquid_glass_capable_swift_for_release() { [[ "$SWIFT_CONFIGURATION" == "release" ]] || return 0