Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,9 @@ final class CaptureHostView: NSView {
controller?.recognizeText()
return
case "s":
guard toolbarItem(.scroll)?.enabled == true else {
return
}
controller?.startScrollCapture(source: "keyboard_s")
return
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import RsnapHostBridge
private struct FrozenSelectionImageRenderRequest: @unchecked Sendable {
let captureID: UInt64
let selection: CGRect
let scrollExportImage: CGImage?
let scrollExportSnapshot: RGBARegionSnapshot?
let frozenDisplayFrame: CGRect?
let frozenDisplayImage: CGImage?
let frozenBaseImage: CGImage?
Expand All @@ -25,8 +25,8 @@ private struct FrozenSelectionImageRenderRequest: @unchecked Sendable {
FrozenPreparedExportKey(
captureID: captureID,
selection: selection,
scrollExportWidth: scrollExportImage?.width ?? 0,
scrollExportHeight: scrollExportImage?.height ?? 0,
scrollExportWidth: scrollExportSnapshot?.width ?? 0,
scrollExportHeight: scrollExportSnapshot?.height ?? 0,
frozenDisplayFrame: frozenDisplayFrame,
frozenBaseWidth: frozenBaseImage?.width ?? 0,
frozenBaseHeight: frozenBaseImage?.height ?? 0,
Expand All @@ -45,7 +45,7 @@ private struct FrozenSelectionImageRenderRequest: @unchecked Sendable {
var canPrepareExportInBackground: Bool {
// Scroll capture exports change as the stitched document grows; prepare those on demand
// until the scroll pipeline exposes a stable export revision.
scrollExportImage == nil
scrollExportSnapshot == nil
}
}

Expand Down Expand Up @@ -273,8 +273,8 @@ extension CaptureSessionController {
guard let selection = currentFrozenSelection() else {
return nil
}
let scrollExportImage =
scrollCaptureState == nil ? nil : try activeScrollCaptureExportImage()
let scrollExportSnapshot =
scrollCaptureState == nil ? nil : try activeScrollCaptureExportSnapshot()
let settings = settingsStore.settings
let selectionCenter = CGPoint(x: selection.midX, y: selection.midY)
let screen = screen(containing: selectionCenter)
Expand All @@ -285,7 +285,7 @@ extension CaptureSessionController {
return FrozenSelectionImageRenderRequest(
captureID: currentCaptureTelemetryID,
selection: selection,
scrollExportImage: scrollExportImage,
scrollExportSnapshot: scrollExportSnapshot,
frozenDisplayFrame: chromeState.frozenDisplayFrame,
frozenDisplayImage: chromeState.frozenDisplayImage,
frozenBaseImage: chromeState.frozenBaseImage,
Expand Down Expand Up @@ -603,20 +603,14 @@ extension CaptureSessionController {
)
}

func activeScrollCaptureExportImage() throws -> CGImage? {
func activeScrollCaptureExportSnapshot() throws -> RGBARegionSnapshot? {
guard Self.scrollCaptureEnabled else {
return nil
}
guard let state = scrollCaptureState else {
return nil
}
guard
let export = try state.stitcher.exportImage(),
let exportImage = NativeHostImageBridge.cgImage(from: export)
else {
return nil
}
return exportImage
return try state.stitcher.exportImage()
}

func captureFrozenSelectionImage(applyingCaptureFrameEffect: Bool = false) throws
Expand Down Expand Up @@ -658,7 +652,7 @@ extension CaptureSessionController {
prefersPixelSnapshot: Bool = false
) throws -> FrozenSelectionImageRenderResult {
let captureStartedAt = ProcessInfo.processInfo.systemUptime
if let scrollExport = request.scrollExportImage {
if let scrollExport = request.scrollExportSnapshot {
return renderScrollExportImage(
scrollExport,
request: request,
Expand All @@ -676,21 +670,22 @@ extension CaptureSessionController {
}

nonisolated private static func renderScrollExportImage(
_ scrollExport: CGImage,
_ scrollExport: RGBARegionSnapshot,
request: FrozenSelectionImageRenderRequest,
captureStartedAt: TimeInterval,
applyingCaptureFrameEffect: Bool,
prefersPixelSnapshot: Bool
) -> FrozenSelectionImageRenderResult {
let base = FrozenRenderedImage(image: nil, rgbaSnapshot: scrollExport)
let result =
applyingCaptureFrameEffect
? applyCaptureFrameEffectIfNeeded(
to: scrollExport,
to: base,
request: request,
hasOverlayEdits: false,
prefersPixelSnapshot: prefersPixelSnapshot
)
: FrozenRenderedImage(image: scrollExport, rgbaSnapshot: nil)
: resolvedRenderedImage(base, prefersPixelSnapshot: prefersPixelSnapshot)
logFrozenSelectionImageTiming(
request: request,
captureStartedAt: captureStartedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ extension CaptureSessionController {
if chromeState.frozenSelectionEditable == false {
return "not_dragged_region"
}
if let selection = currentFrozenSelection(),
scrollCaptureSelectionHasSufficientHeight(selection) == false
{
return "selection_too_short"
}
return "unavailable"
}

Expand All @@ -105,18 +110,24 @@ extension CaptureSessionController {
return "Scroll Capture requires a dragged region selection."
case "no_selection", "requires_frozen":
return "Select a dragged region before starting Scroll Capture."
case "selection_too_short":
return "Select a taller region before starting Scroll Capture."
default:
return "Scroll Capture is not available for this selection."
}
}

private func scrollCaptureEntryDetail(source: String, reason: String) -> String {
[
let selection = currentFrozenSelection()

return [
"source=\(source)",
"reason=\(reason)",
"scene=\(scene.mode)",
"editable=\(chromeState.frozenSelectionEditable)",
"has_selection=\(currentFrozenSelection() != nil)",
"has_selection=\(selection != nil)",
"selection_height_px=\(selection.map { scrollCaptureSelectionHeightPixels($0) } ?? 0)",
"minimum_height_px=\(Self.scrollCaptureMinimumSelectionHeightPixels)",
"active=\(scrollCaptureState != nil)",
].joined(separator: " ")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,15 @@ extension CaptureSessionController {
private static let scrollCaptureQueuedWheelDeltaLimitMultiplier = 32.0

var scrollCaptureToolbarEnabled: Bool {
Self.scrollCaptureEnabled
&& scene.mode == .frozen
&& scrollCaptureState == nil
&& chromeState.frozenSelectionEditable
&& currentFrozenSelection() != nil
guard Self.scrollCaptureEnabled,
scene.mode == .frozen,
scrollCaptureState == nil,
chromeState.frozenSelectionEditable,
let selection = currentFrozenSelection()
else {
return false
}
return scrollCaptureSelectionHasSufficientHeight(selection)
}

func handleScrollCaptureWheel(_ event: NSEvent, at point: CGPoint) -> Bool {
Expand Down Expand Up @@ -476,6 +480,11 @@ extension CaptureSessionController {
refreshOverlay()
return
}
guard scrollCaptureSelectionHasSufficientHeight(selection) else {
try setHostStatusMessage("Select a taller region before starting Scroll Capture.")
refreshOverlay()
return
}

guard
let captureSource = overlayController?.scrollCaptureFallbackSource(
Expand Down Expand Up @@ -1098,6 +1107,24 @@ extension CaptureSessionController {
return scrollCaptureFlippedDesktopPoint(fallbackAppKitPoint)
}

func scrollCaptureSelectionHasSufficientHeight(_ selection: CGRect) -> Bool {
scrollCaptureSelectionHeightPixels(selection)
>= Self.scrollCaptureMinimumSelectionHeightPixels
}

func scrollCaptureSelectionHeightPixels(_ selection: CGRect) -> Int {
if chromeState.frozenSelectionSnapshot == selection,
let frozenBaseImage = chromeState.frozenBaseImage
{
return frozenBaseImage.height
}
let point = CGPoint(x: selection.midX, y: selection.midY)
let scale =
screen(containing: point)?.backingScaleFactor
?? NSScreen.main?.backingScaleFactor
?? 1
return Int((selection.height * scale).rounded())
}
}

private func scrollCaptureFlippedDesktopPoint(_ point: CGPoint) -> CGPoint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class CaptureSessionController: NSObject {
static let displayFirstFrameWait: TimeInterval = 0.025
static let coldSelfCaptureRecoveryWait: TimeInterval = 3.5
static let scrollCaptureEnabled = true
static let scrollCaptureMinimumSelectionHeightPixels = 120
static let scrollCaptureForwardingPassthrough: TimeInterval = 0.012
static let scrollCaptureControlledScrollSettleDelay: TimeInterval = 0.18
static let scrollCaptureInputLiveFrameMaxAge: TimeInterval = 0.18
Expand Down
28 changes: 19 additions & 9 deletions packages/rsnap-host-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1432,10 +1432,13 @@ pub unsafe extern "C" fn rsnap_scroll_session_observe_downward_frame(
Ok(outcome) => outcome,
Err(_err) => return RsnapStatus::InvalidInput,
};
let export = handle.session.export_image();
let (export_width, export_height) = handle.session.export_dimensions();

unsafe {
ptr::write(out_result, encode_scroll_observe_result(outcome, &export, &handle.session));
ptr::write(
out_result,
encode_scroll_observe_result(outcome, export_width, export_height, &handle.session),
);
}

RsnapStatus::Ok
Expand Down Expand Up @@ -1481,10 +1484,13 @@ pub unsafe extern "C" fn rsnap_scroll_session_observe_downward_frame_with_motion
Ok(outcome) => outcome,
Err(_err) => return RsnapStatus::InvalidInput,
};
let export = handle.session.export_image();
let (export_width, export_height) = handle.session.export_dimensions();

unsafe {
ptr::write(out_result, encode_scroll_observe_result(outcome, &export, &handle.session));
ptr::write(
out_result,
encode_scroll_observe_result(outcome, export_width, export_height, &handle.session),
);
}

RsnapStatus::Ok
Expand Down Expand Up @@ -1539,15 +1545,18 @@ pub unsafe extern "C" fn rsnap_scroll_session_undo_last_append(
}

let did_undo = handle.session.undo_last_append();
let export = handle.session.export_image();
let (export_width, export_height) = handle.session.export_dimensions();
let kind = if did_undo {
ScrollStitchObserveOutcome::PreviewUpdated
} else {
ScrollStitchObserveOutcome::NoChange
};

unsafe {
ptr::write(out_result, encode_scroll_observe_result(kind, &export, &handle.session));
ptr::write(
out_result,
encode_scroll_observe_result(kind, export_width, export_height, &handle.session),
);
}

RsnapStatus::Ok
Expand Down Expand Up @@ -3840,7 +3849,8 @@ fn encode_host_request(request: HostRequest) -> RsnapHostRequestValue {

fn encode_scroll_observe_result(
outcome: ScrollStitchObserveOutcome,
export: &ScrollStitchImage,
export_width: u32,
export_height: u32,
session: &ScrollStitchSession,
) -> RsnapScrollObserveResult {
let (kind, growth_rows) = match outcome {
Expand All @@ -3859,8 +3869,8 @@ fn encode_scroll_observe_result(
RsnapScrollObserveResult {
kind: kind as u32,
growth_rows,
export_width: export.width,
export_height: export.height,
export_width,
export_height,
current_viewport_top_y: session.current_viewport_top_y(),
}
}
Expand Down
Loading