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
31 changes: 31 additions & 0 deletions native/macos-host/Sources/RsnapNativeHostKit/CaptureGeometry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import CoreGraphics

extension CGRect {
package func inclusivelyContains(_ point: CGPoint) -> Bool {
!isNull && !isInfinite
&& point.x >= minX && point.x <= maxX
&& point.y >= minY && point.y <= maxY
}

package func clampedInclusivePoint(_ point: CGPoint) -> CGPoint? {
guard inclusivelyContains(point) else {
return nil
}
return CGPoint(
x: point.x.clamped(to: minX...maxX),
y: point.y.clamped(to: minY...maxY)
)
}
}

package func captureOverlayLocalPoint(
from globalPoint: CGPoint,
windowFrame: CGRect,
bounds: CGRect
) -> CGPoint? {
let local = CGPoint(
x: globalPoint.x - windowFrame.minX,
y: globalPoint.y - windowFrame.minY
)
return bounds.clampedInclusivePoint(local)
}
Original file line number Diff line number Diff line change
Expand Up @@ -1605,19 +1605,19 @@ final class CaptureHostView: NSView {
guard let window else {
return nil
}
let local = CGPoint(
x: globalPoint.x - window.frame.minX,
y: globalPoint.y - window.frame.minY
return captureOverlayLocalPoint(
from: globalPoint,
windowFrame: window.frame,
bounds: bounds
)
return bounds.contains(local) ? local : nil
}

private func currentLocalMousePoint() -> CGPoint? {
guard let window else {
return nil
}
let localPoint = window.mouseLocationOutsideOfEventStream
return bounds.contains(localPoint) ? localPoint : nil
return bounds.clampedInclusivePoint(localPoint)
}

private func currentCursorPresentation() -> CursorPresentation {
Expand Down Expand Up @@ -1765,7 +1765,7 @@ final class CaptureHostView: NSView {
}
let localPoint = window.mouseLocationOutsideOfEventStream
let globalPoint = window.convertPoint(toScreen: localPoint)
return NSScreen.screens.contains(where: { $0.frame.contains(globalPoint) })
return NSScreen.screens.contains(where: { $0.frame.inclusivelyContains(globalPoint) })
? globalPoint : nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ final class CaptureOverlayController {
settings: settings
)
windows.append(window)
if targetWindow == nil, screen.frame.contains(focusPoint) {
if targetWindow == nil, screen.frame.inclusivelyContains(focusPoint) {
targetWindow = window
}
}
Expand Down Expand Up @@ -213,7 +213,9 @@ final class CaptureOverlayController {
}

func focusWindow(at point: CGPoint) {
guard let targetWindow = windows.first(where: { $0.frame.contains(point) }) ?? windows.first
guard
let targetWindow = windows.first(where: { $0.frame.inclusivelyContains(point) })
?? windows.first
else {
return
}
Expand Down Expand Up @@ -355,14 +357,14 @@ final class CaptureOverlayController {
}

func hoverWindow(at point: CGPoint) -> WindowSnapshot? {
guard NSScreen.screens.contains(where: { $0.frame.contains(point) }) else {
guard NSScreen.screens.contains(where: { $0.frame.inclusivelyContains(point) }) else {
return nil
}
return windowSnapshotFeed.window(at: point)
}

func hoverWindowPreview(at point: CGPoint) -> WindowSnapshot? {
guard NSScreen.screens.contains(where: { $0.frame.contains(point) }) else {
guard NSScreen.screens.contains(where: { $0.frame.inclusivelyContains(point) }) else {
return nil
}
return windowSnapshotFeed.window(at: point)
Expand Down Expand Up @@ -472,7 +474,7 @@ final class CaptureOverlayController {
near point: CGPoint
) -> CaptureSessionController.FrozenCaptureJobSource? {
guard
let referenceWindow = windows.first(where: { $0.frame.contains(point) })
let referenceWindow = windows.first(where: { $0.frame.inclusivelyContains(point) })
?? windows.first
else {
return nil
Expand All @@ -492,13 +494,13 @@ final class CaptureOverlayController {

fileprivate func liveColorSampleSource(near point: CGPoint) -> LiveColorSampleSource? {
guard
let referenceWindow = windows.first(where: { $0.frame.contains(point) })
let referenceWindow = windows.first(where: { $0.frame.inclusivelyContains(point) })
?? windows.first
else {
return nil
}
let screen =
NSScreen.screens.first(where: { $0.frame.contains(point) })
NSScreen.screens.first(where: { $0.frame.inclusivelyContains(point) })
?? referenceWindow.screen
guard let displayID = screen.flatMap(Self.displayID) else {
return nil
Expand Down Expand Up @@ -827,7 +829,7 @@ final class CaptureOverlayController {

let focusPoint = CGPoint(x: selection.midX, y: selection.midY)
guard
let primaryWindow = windows.first(where: { $0.frame.contains(focusPoint) })
let primaryWindow = windows.first(where: { $0.frame.inclusivelyContains(focusPoint) })
?? windows.first
else {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import RsnapHostBridge

extension CaptureSessionController {
func screen(containing point: CGPoint) -> NSScreen? {
NSScreen.screens.first(where: { $0.frame.contains(point) })
NSScreen.screens.first(where: { $0.frame.inclusivelyContains(point) })
}

func activeMonitor(at point: CGPoint) -> MonitorSnapshot? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1367,9 +1367,3 @@ private func scrollCaptureFlippedDesktopPoint(_ point: CGPoint) -> CGPoint {
y: desktop.minY + desktop.maxY - point.y
)
}

extension CGRect {
fileprivate func inclusivelyContains(_ point: CGPoint) -> Bool {
point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,10 @@ final class FrozenFrameAuthority: @unchecked Sendable {
defer {
stateLock.unlock()
}
guard let displayID = displayTargets.first(where: { $0.value.frame.contains(point) })?.key
guard
let displayID = displayTargets.first(where: {
$0.value.frame.inclusivelyContains(point)
})?.key
else {
return nil
}
Expand All @@ -783,7 +786,10 @@ final class FrozenFrameAuthority: @unchecked Sendable {
guard selfCaptureFilterRequired else {
return false
}
guard let displayID = displayTargets.first(where: { $0.value.frame.contains(point) })?.key
guard
let displayID = displayTargets.first(where: {
$0.value.frame.inclusivelyContains(point)
})?.key
else {
return false
}
Expand All @@ -800,7 +806,10 @@ final class FrozenFrameAuthority: @unchecked Sendable {
defer {
stateLock.unlock()
}
guard let displayID = displayTargets.first(where: { $0.value.frame.contains(point) })?.key,
guard
let displayID = displayTargets.first(where: {
$0.value.frame.inclusivelyContains(point)
})?.key,
let record = latestFrames[displayID],
let eligibleRecord = snapshotEligibleRecordLocked(record)
else {
Expand All @@ -817,7 +826,10 @@ final class FrozenFrameAuthority: @unchecked Sendable {
guard selfCaptureFilterRequired else {
return false
}
guard let displayID = displayTargets.first(where: { $0.value.frame.contains(point) })?.key,
guard
let displayID = displayTargets.first(where: {
$0.value.frame.inclusivelyContains(point)
})?.key,
let stream = streams[displayID]
else {
return false
Expand All @@ -831,7 +843,8 @@ final class FrozenFrameAuthority: @unchecked Sendable {

func liveRgbSample(containing point: CGPoint) -> LiveRgbSample? {
stateLock.lock()
let displayID = displayTargets.first(where: { $0.value.frame.contains(point) })?.key
let displayID = displayTargets.first(where: { $0.value.frame.inclusivelyContains(point) })?
.key
let record = displayID.flatMap { latestFrames[$0] }.flatMap(snapshotEligibleRecordLocked)
stateLock.unlock()
guard let record else {
Expand All @@ -858,7 +871,8 @@ final class FrozenFrameAuthority: @unchecked Sendable {

func loupePatch(containing point: CGPoint, sidePixels: Int) -> CGImage? {
stateLock.lock()
let displayID = displayTargets.first(where: { $0.value.frame.contains(point) })?.key
let displayID = displayTargets.first(where: { $0.value.frame.inclusivelyContains(point) })?
.key
let record = displayID.flatMap { latestFrames[$0] }.flatMap(snapshotEligibleRecordLocked)
stateLock.unlock()
guard let record else {
Expand All @@ -880,7 +894,8 @@ final class FrozenFrameAuthority: @unchecked Sendable {
let deadline = Date(timeIntervalSinceNow: max(0, maxWait))
stateLock.lock()
let displayID =
token?.displayID ?? displayTargets.first(where: { $0.value.frame.contains(point) })?.key
token?.displayID
?? displayTargets.first(where: { $0.value.frame.inclusivelyContains(point) })?.key
guard let displayID else {
stateLock.unlock()
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ final class LiveFrameStreamBroker: @unchecked Sendable {
Self.monitorSnapshot(for: $0, mainDisplayHeight: mainDisplayHeight)
}
let targetMonitor = prewarmPoint.flatMap { point in
nextMonitors.first(where: { $0.appKitFrame.contains(point) })
nextMonitors.first(where: { $0.appKitFrame.inclusivelyContains(point) })
}
let monitorsUnchanged = nextMonitors == monitors
monitors = nextMonitors
Expand Down Expand Up @@ -308,7 +308,7 @@ final class LiveFrameStreamBroker: @unchecked Sendable {
stateLock.lock()
let monitors = self.monitors
stateLock.unlock()
return monitors.first(where: { $0.appKitFrame.contains(point) })
return monitors.first(where: { $0.appKitFrame.inclusivelyContains(point) })
}

private static func makeSampler(exceptionWindowIDs: Set<CGWindowID>) -> RsnapLiveSampler? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ final class WindowSnapshotFeed {
stateLock.lock()
let snapshots = latestSnapshots
stateLock.unlock()
return snapshots.first(where: { $0.frame.contains(point) })
return snapshots.first(where: { $0.frame.inclusivelyContains(point) })
}

static func snapshots(desktopFrame: CGRect) -> [WindowSnapshot] {
Expand Down Expand Up @@ -137,7 +137,7 @@ final class WindowSnapshotFeed {
}

static func window(at point: CGPoint, in snapshots: [WindowSnapshot]) -> WindowSnapshot? {
snapshots.first(where: { $0.frame.contains(point) })
snapshots.first(where: { $0.frame.inclusivelyContains(point) })
}

private func refresh() {
Expand Down
36 changes: 36 additions & 0 deletions native/macos-host/Sources/RsnapNativeHostKitProbe/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum RsnapNativeHostKitProbe {
assertScrimExclusionPreservesExistingPixels()
assertRoundedExclusionMaskKeepsCornersFilled()
assertCaptureFrameEffectExpandsExportCanvas()
assertCaptureOverlayLocalPointKeepsScreenEdgesVisible()
assertScrollCaptureViewportPointAcceptsFlippedGlobalMouseCoordinates()
assertScrollCaptureObservedInputAcceptsSourceWindowGutter()
assertSoftwareUpdateModeResolution()
Expand Down Expand Up @@ -97,6 +98,41 @@ enum RsnapNativeHostKitProbe {
}
}

private static func assertCaptureOverlayLocalPointKeepsScreenEdgesVisible() {
let windowFrame = CGRect(x: 0, y: 0, width: 1_440, height: 900)
let bounds = CGRect(x: 0, y: 0, width: 1_440, height: 900)

guard
captureOverlayLocalPoint(
from: CGPoint(x: windowFrame.maxX, y: windowFrame.maxY),
windowFrame: windowFrame,
bounds: bounds
) == CGPoint(x: bounds.maxX, y: bounds.maxY)
else {
fatalError("capture overlay should keep HUD placement alive at screen max edges")
}

guard
captureOverlayLocalPoint(
from: CGPoint(x: windowFrame.minX, y: windowFrame.minY),
windowFrame: windowFrame,
bounds: bounds
) == CGPoint(x: bounds.minX, y: bounds.minY)
else {
fatalError("capture overlay should keep HUD placement alive at screen min edges")
}

guard
captureOverlayLocalPoint(
from: CGPoint(x: windowFrame.maxX + 1, y: windowFrame.midY),
windowFrame: windowFrame,
bounds: bounds
) == nil
else {
fatalError("capture overlay should still reject points outside the screen edge")
}
}

private static func assertRectEqual(_ actual: CGRect, _ expected: CGRect, _ message: String) {
guard nearlyEqual(actual.origin.x, expected.origin.x),
nearlyEqual(actual.origin.y, expected.origin.y),
Expand Down