oficina / meet Public

Merge fix/window-resize-move: drag the picture to move the call window

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5987bfe2534bba16b6f7fa0707fef97b669e8b57 parent 05cf0cdparent 718f30e
5 files changed, +467 −8
  • native/DESIGN.md +6 −1
  • native/apple/Sources/CallsKit/MacFullWindowCall.swift +3 −7
  • native/apple/Sources/CallsKit/MacVideoTile.swift +6 −0
  • native/apple/Sources/CallsKit/WindowMove.swift +226 −0
  • native/apple/Tests/CallsKitTests/WindowMoveTests.swift +226 −0
diff --git a/native/DESIGN.md b/native/DESIGN.md
index 1dab73a..91d7222 100644
--- a/native/DESIGN.md
+++ b/native/DESIGN.md
@@ -109,7 +109,12 @@ Full window is the default. The mini window and tab are unchanged.
over it and fade with the controls (not in full screen, where the system
shows them), a shade behind them keeps them legible, and a 32 pt strip
across the top drags the window and double-clicks as the person's title-bar
- setting says (`WindowDragStrip`, `TitleBarDoubleClick`). The title pill sits
+ setting says (`WindowDragStrip`, `TitleBarDoubleClick`). Since 0.3.7 the
+ picture does too, as in FaceTime (`WindowMove`): a drag anywhere on the
+ stage that is not a control, a panel or the self view moves the window
+ once it has gone 4 pt (less is a click, which shows the controls), and a
+ double click there does what the title bar's would. Resizing is AppKit's
+ own, from every edge and corner, the top included. The title pill sits
to the right of the traffic lights and the notices in a column beneath it.
`CallWindowChrome` sets the window's title bar, watches it so SwiftUI cannot
quietly restore it mid-call, and puts it back for the lobby and Classic.
diff --git a/native/apple/Sources/CallsKit/MacFullWindowCall.swift b/native/apple/Sources/CallsKit/MacFullWindowCall.swift
index 0816cf8..b8a6f0e 100644
--- a/native/apple/Sources/CallsKit/MacFullWindowCall.swift
+++ b/native/apple/Sources/CallsKit/MacFullWindowCall.swift
@@ -34,9 +34,9 @@
ZStack {
Color.black
if let fixtureBackdrop {
- fixtureBackdrop.view.accessibilityHidden(true)
+ fixtureBackdrop.view.accessibilityHidden(true).movesWindow()
} else {
- fullWindowStage(in: size)
+ fullWindowStage(in: size).movesWindow()
}
if !grid { selfView(in: size) }
topShade
@@ -611,11 +611,7 @@
override func mouseDown(with event: NSEvent) {
guard let window else { return }
if event.clickCount == 2 {
- switch TitleBarDoubleClick.current() {
- case .zoom: window.zoom(nil)
- case .minimize: window.miniaturize(nil)
- case .none: break
- }
+ TitleBarDoubleClick.current().perform(on: window)
} else {
window.performDrag(with: event)
}
diff --git a/native/apple/Sources/CallsKit/MacVideoTile.swift b/native/apple/Sources/CallsKit/MacVideoTile.swift
index 38f9380..2bb58cd 100644
--- a/native/apple/Sources/CallsKit/MacVideoTile.swift
+++ b/native/apple/Sources/CallsKit/MacVideoTile.swift
@@ -112,6 +112,12 @@
}
required init?(coder: NSCoder) { nil }
+ // A picture takes no clicks: they go to the SwiftUI view it is in, so
+ // a drag on it moves the window (WindowMove) or the self view, as a
+ // drag on a tile with no picture does, the click that brings the
+ // window forward included.
+ override func hitTest(_ point: NSPoint) -> NSView? { nil }
+
func attach(_ next: RTCVideoTrack) {
guard track !== next else { return }
detach()
diff --git a/native/apple/Sources/CallsKit/WindowMove.swift b/native/apple/Sources/CallsKit/WindowMove.swift
new file mode 100644
index 0000000..5255f8e
--- /dev/null
+++ b/native/apple/Sources/CallsKit/WindowMove.swift
@@ -0,0 +1,226 @@
+#if os(macOS)
+ import AppKit
+ import SwiftUI
+
+ /// Moving the full-window call by its picture, as FaceTime does. With no
+ /// title bar, the 32 pt strip across the top (`WindowDragStrip`) was the
+ /// only place a window could be picked up by; now a press anywhere on the
+ /// stage that is not something of its own — the bar, a panel, a notice,
+ /// the self view, which drags itself, a tile's ⓘ — moves the window once
+ /// the pointer has gone `threshold` points with the button down. Less
+ /// than that is a click, which shows the controls as any click does.
+ ///
+ /// Resizing needs nothing from here: AppKit keeps the few points inside
+ /// every edge (and more at the corners) for itself, above any content,
+ /// as long as the window is `.resizable` — which `CallWindowChrome`
+ /// leaves alone. `WindowMoveTests` holds both.
+ enum WindowMove {
+ /// How far the pointer goes with the button down before a press on
+ /// the picture is a drag rather than a click: a shaky click stays a
+ /// click. The mini window uses the same slack.
+ static let threshold: CGFloat = 4
+
+ /// Whether a press that went down at `start` and is now at `now` is a
+ /// drag.
+ static func isDrag(from start: CGPoint, to now: CGPoint) -> Bool {
+ hypot(now.x - start.x, now.y - start.y) >= threshold
+ }
+
+ /// Whether the window can be moved at all: not in full screen, where it
+ /// is the screen, and not when AppKit says it is not movable.
+ static func canMove(fullScreen: Bool, movable: Bool) -> Bool {
+ !fullScreen && movable
+ }
+
+ /// The window's frame origin for the pointer at `pointer`, when it was
+ /// at `origin` with the pointer at `grabbed` (all in screen points).
+ static func origin(from origin: CGPoint, grabbed: CGPoint, pointer: CGPoint) -> CGPoint {
+ CGPoint(x: origin.x + pointer.x - grabbed.x, y: origin.y + pointer.y - grabbed.y)
+ }
+
+ /// One press on the picture: a click until it passes the threshold,
+ /// then where the window should be for each position of the pointer.
+ struct Press: Equatable {
+ let origin: CGPoint
+ let grabbed: CGPoint
+ private(set) var moving = false
+
+ init(origin: CGPoint, grabbed: CGPoint) {
+ self.origin = origin
+ self.grabbed = grabbed
+ }
+
+ /// Nil while it is still a click.
+ mutating func moved(to pointer: CGPoint) -> CGPoint? {
+ if !moving, !WindowMove.isDrag(from: grabbed, to: pointer) { return nil }
+ moving = true
+ return WindowMove.origin(from: origin, grabbed: grabbed, pointer: pointer)
+ }
+ }
+ }
+
+ extension View {
+ /// A press and drag on this view moves its window (see `WindowMove`).
+ /// Views above it with gestures or controls of their own keep them.
+ func movesWindow() -> some View { modifier(WindowMoveModifier()) }
+
+ /// Lets the click that brings the window forward reach this view's
+ /// gestures, as it reaches a title bar (macOS 15 and later). Given to
+ /// the stage by `movesWindow`; a scroll view's content needs it again,
+ /// because it does not reach through one.
+ func takesActivatingClicks() -> some View { modifier(ActivatingClicks()) }
+ }
+
+ private struct WindowMoveModifier: ViewModifier {
+ @State private var mover = WindowMover()
+
+ func body(content: Content) -> some View {
+ content
+ // Every point of it, the gaps between tiles included, not only
+ // where something is drawn.
+ .contentShape(Rectangle())
+ .gesture(
+ // Only a drag: a click on the picture is left alone, and shows
+ // the controls as it always has (CallWindowActivity).
+ DragGesture(minimumDistance: WindowMove.threshold, coordinateSpace: .global)
+ .onChanged { _ in mover.begin() }
+ .onEnded { _ in mover.end() }
+ )
+ // What a double click on the title bar does, as on the strip that
+ // stands in for it: zoom, by default.
+ .onTapGesture(count: 2) { mover.doubleClicked() }
+ .takesActivatingClicks()
+ .background(WindowFinder(mover: mover))
+ }
+ }
+
+ private struct ActivatingClicks: ViewModifier {
+ func body(content: Content) -> some View {
+ if #available(macOS 15, *) {
+ content.allowsWindowActivationEvents(true)
+ } else {
+ content
+ }
+ }
+ }
+
+ /// Moves the window for a drag on the picture. The gesture is used only
+ /// to say that one has begun, on the picture and not on anything over
+ /// it: its values are in the view's coordinates, which shift as the
+ /// window does, and once the window followed the pointer SwiftUI stopped
+ /// reporting the drag after its first step. So the window's own mouse
+ /// events are watched instead, where the pointer is on screen, and
+ /// passed on untouched.
+ @MainActor final class WindowMover {
+ weak var window: NSWindow? {
+ didSet { if window !== oldValue { watch() } }
+ }
+ /// Where on screen the button last went down in the window.
+ private var down: CGPoint?
+ private var press: WindowMove.Press?
+ private var monitor: Any?
+
+ /// A drag on the picture has passed the threshold.
+ func begin() {
+ guard press == nil, let window, let down,
+ WindowMove.canMove(
+ fullScreen: window.styleMask.contains(.fullScreen), movable: window.isMovable)
+ else { return }
+ // The window has not moved since the button went down. It follows
+ // from the next drag event on.
+ press = WindowMove.Press(origin: window.frame.origin, grabbed: down)
+ }
+
+ func doubleClicked() {
+ guard let window, !window.styleMask.contains(.fullScreen) else { return }
+ TitleBarDoubleClick.current().perform(on: window)
+ }
+
+ func end() { press = nil }
+
+ func stop() {
+ if let monitor { NSEvent.removeMonitor(monitor) }
+ monitor = nil
+ press = nil
+ }
+
+ private func watch() {
+ stop()
+ guard window != nil else { return }
+ monitor = NSEvent.addLocalMonitorForEvents(
+ matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]
+ ) { [weak self] event in
+ nonisolated(unsafe) let event = event
+ MainActor.assumeIsolated {
+ guard let self, event.window === self.window else { return }
+ switch event.type {
+ case .leftMouseDown: self.down = Self.screenLocation(of: event)
+ case .leftMouseDragged: self.move(to: Self.screenLocation(of: event))
+ default: self.end()
+ }
+ }
+ return event
+ }
+ }
+
+ private func move(to pointer: CGPoint) {
+ guard let window, let origin = press?.moved(to: pointer) else { return }
+ let screen = NSScreen.screens.first { $0.frame.contains(pointer) } ?? window.screen
+ let frame = window.constrainFrameRect(
+ NSRect(origin: origin, size: window.frame.size), to: screen)
+ window.setFrameOrigin(frame.origin)
+ }
+
+ /// Where an event happened on screen, in AppKit's screen points (y up
+ /// from the bottom of the main display). From the event itself, not
+ /// its place in the window: the window is moving under the pointer,
+ /// and an event queued before a move is placed against where the
+ /// window was.
+ private static func screenLocation(of event: NSEvent) -> CGPoint {
+ guard let global = event.cgEvent?.location, let main = NSScreen.screens.first else {
+ return NSEvent.mouseLocation
+ }
+ return CGPoint(x: global.x, y: main.frame.maxY - global.y)
+ }
+
+ deinit {
+ if let monitor { NSEvent.removeMonitor(monitor) }
+ }
+ }
+
+ extension TitleBarDoubleClick {
+ @MainActor func perform(on window: NSWindow) {
+ switch self {
+ case .zoom: window.zoom(nil)
+ case .minimize: window.miniaturize(nil)
+ case .none: break
+ }
+ }
+ }
+
+ /// Finds the window a SwiftUI view is in, for the mover. Takes no
+ /// clicks.
+ private struct WindowFinder: NSViewRepresentable {
+ let mover: WindowMover
+
+ func makeNSView(context: Context) -> FinderView {
+ let view = FinderView()
+ view.mover = mover
+ return view
+ }
+ func updateNSView(_ view: FinderView, context: Context) {
+ view.mover = mover
+ mover.window = view.window
+ }
+ static func dismantleNSView(_ view: FinderView, coordinator: ()) { view.mover?.window = nil }
+
+ final class FinderView: NSView {
+ var mover: WindowMover?
+ override func viewDidMoveToWindow() {
+ super.viewDidMoveToWindow()
+ mover?.window = window
+ }
+ override func hitTest(_ point: NSPoint) -> NSView? { nil }
+ }
+ }
+#endif
diff --git a/native/apple/Tests/CallsKitTests/WindowMoveTests.swift b/native/apple/Tests/CallsKitTests/WindowMoveTests.swift
new file mode 100644
index 0000000..43ce68b
--- /dev/null
+++ b/native/apple/Tests/CallsKitTests/WindowMoveTests.swift
@@ -0,0 +1,226 @@
+#if os(macOS)
+ import AppKit
+ import SwiftUI
+ import XCTest
+
+ import LiveKitWebRTC
+
+ @testable @_spi(Fixtures) import CallsKit
+
+ /// Paul, on 0.3.6: "I should be able to click anywhere on the window to
+ /// resize it." The full-window call has had no title bar since 0.3.5, so
+ /// moving it is the picture's job, and resizing must still work from
+ /// every edge, the top included.
+ final class WindowMoveRuleTests: XCTestCase {
+ func testAShortPressIsAClickAndALongerOneADrag() {
+ XCTAssertEqual(WindowMove.threshold, 4)
+ let start = CGPoint(x: 100, y: 100)
+ XCTAssertFalse(WindowMove.isDrag(from: start, to: start))
+ XCTAssertFalse(WindowMove.isDrag(from: start, to: CGPoint(x: 102, y: 102)), "a shaky click")
+ XCTAssertTrue(WindowMove.isDrag(from: start, to: CGPoint(x: 104, y: 100)))
+ XCTAssertTrue(WindowMove.isDrag(from: start, to: CGPoint(x: 97, y: 97)), "any direction")
+ }
+
+ func testThePressFollowsThePointerOnceItIsADrag() {
+ var press = WindowMove.Press(origin: CGPoint(x: 200, y: 300), grabbed: CGPoint(x: 700, y: 650))
+ XCTAssertNil(press.moved(to: CGPoint(x: 702, y: 651)), "still a click")
+ XCTAssertFalse(press.moving)
+ XCTAssertEqual(press.moved(to: CGPoint(x: 740, y: 625)), CGPoint(x: 240, y: 275))
+ XCTAssertTrue(press.moving)
+ // Once moving, it goes wherever the pointer goes, back to where it
+ // started included: the window is not left 4 pt short.
+ XCTAssertEqual(press.moved(to: CGPoint(x: 701, y: 650)), CGPoint(x: 201, y: 300))
+ XCTAssertEqual(press.moved(to: CGPoint(x: 700, y: 650)), CGPoint(x: 200, y: 300))
+ }
+
+ func testOnlyAWindowThatCanMoveIsMoved() {
+ XCTAssertTrue(WindowMove.canMove(fullScreen: false, movable: true))
+ XCTAssertFalse(WindowMove.canMove(fullScreen: true, movable: true), "full screen is the screen")
+ XCTAssertFalse(WindowMove.canMove(fullScreen: false, movable: false))
+ }
+ }
+
+ /// The real full-window call in a titled, resizable window like the app's,
+ /// driven with mouse events sent to it: what a press on each part of it
+ /// does to the window. The window is never made key or active: a call
+ /// window behind another app's must move by its picture too.
+ @MainActor final class CallWindowMouseTests: XCTestCase {
+ private var window: NSWindow!
+ private let start = NSRect(x: 200, y: 200, width: 1000, height: 700)
+
+ override func setUp() async throws {
+ NSApplication.shared.setActivationPolicy(.prohibited)
+ UserDefaults.standard.removeObject(forKey: PiPCorner.key)
+ }
+
+ override func tearDown() async throws {
+ window?.close()
+ window = nil
+ UserDefaults.standard.removeObject(forKey: PiPCorner.key)
+ }
+
+ private func open(_ fixture: CallFixture = .focus, grid: Bool = false, controls: Bool = false) {
+ let call = Call(playAudio: false)
+ fixture.apply(to: call)
+ window = NSWindow(
+ contentRect: start, styleMask: [.titled, .closable, .miniaturizable, .resizable],
+ backing: .buffered, defer: false)
+ window.isReleasedWhenClosed = false
+ window.contentView = NSHostingView(
+ rootView: MacCallView(
+ call: call, grid: grid, style: .fullWindow, controlsShown: controls, leave: {}, rejoin: {}))
+ window.orderFrontRegardless()
+ pump(0.8)
+ window.setFrame(start, display: true)
+ pump(0.2)
+ }
+
+ private func pump(_ seconds: Double) {
+ let until = Date(timeIntervalSinceNow: seconds)
+ while let event = NSApp.nextEvent(
+ matching: .any, until: until, inMode: .default, dequeue: true)
+ {
+ NSApp.sendEvent(event)
+ }
+ }
+
+ private func event(_ type: NSEvent.EventType, at point: NSPoint, clicks: Int = 1) -> NSEvent {
+ NSEvent.mouseEvent(
+ with: type, location: point, modifierFlags: [],
+ timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
+ context: nil, eventNumber: 0, clickCount: clicks, pressure: type == .leftMouseUp ? 0 : 1)!
+ }
+
+ /// Presses at `point` (window coordinates, y up), drags by `by` in
+ /// steps as a hand would, and lets go; returns how far the window went.
+ private func drag(from point: NSPoint, by: CGVector) -> CGVector {
+ let before = window.frame.origin
+ let from = window.convertPoint(toScreen: point)
+ NSApp.sendEvent(event(.leftMouseDown, at: point))
+ pump(0.05)
+ for step in 1...6 {
+ let part = CGFloat(step) / 6
+ let there = NSPoint(x: from.x + by.dx * part, y: from.y + by.dy * part)
+ NSApp.sendEvent(event(.leftMouseDragged, at: window.convertPoint(fromScreen: there)))
+ pump(0.03)
+ }
+ let end = NSPoint(x: from.x + by.dx, y: from.y + by.dy)
+ NSApp.sendEvent(event(.leftMouseUp, at: window.convertPoint(fromScreen: end)))
+ pump(0.2)
+ return CGVector(dx: window.frame.minX - before.x, dy: window.frame.minY - before.y)
+ }
+
+ private var bounds: NSRect { window.contentView!.bounds }
+
+ func testTheCallLeavesTheWindowResizableWithNoTitleBar() {
+ open()
+ XCTAssertTrue(window.styleMask.contains(.resizable))
+ XCTAssertTrue(window.styleMask.contains(.fullSizeContentView))
+ XCTAssertEqual(window.titleVisibility, .hidden)
+ XCTAssertTrue(window.titlebarAppearsTransparent)
+ }
+
+ /// AppKit keeps a few points inside each edge, and more at the corners,
+ /// for resizing, whatever the content — the drag strip along the top
+ /// included. Only past them do the call's own views get the click.
+ func testTheEdgesAndCornersBelongToTheWindow() {
+ open()
+ let frameView = window.contentView!.superview!
+ let edges: [(String, NSPoint)] = [
+ ("top", NSPoint(x: bounds.midX, y: bounds.maxY - 1)),
+ ("bottom", NSPoint(x: bounds.midX, y: 1)),
+ ("left", NSPoint(x: 1, y: bounds.midY)),
+ ("right", NSPoint(x: bounds.maxX - 1, y: bounds.midY)),
+ ("top left", NSPoint(x: 4, y: bounds.maxY - 4)),
+ ("top right", NSPoint(x: bounds.maxX - 4, y: bounds.maxY - 4)),
+ ("bottom left", NSPoint(x: 4, y: 4)),
+ ("bottom right", NSPoint(x: bounds.maxX - 4, y: 4)),
+ ]
+ for (edge, point) in edges {
+ XCTAssertTrue(frameView.hitTest(point) === frameView, "\(edge) is the window's")
+ }
+ XCTAssertTrue(
+ frameView.hitTest(NSPoint(x: bounds.midX, y: bounds.maxY - 12)) is WindowDragStrip.StripView,
+ "below the edge, the strip")
+ }
+
+ func testEveryEdgeAndCornerResizesTheWindow() {
+ open()
+ // (where, drag, which edges should move: minX, maxX, minY, maxY)
+ let cases: [(String, NSPoint, CGVector, [Bool])] = [
+ ("top", NSPoint(x: bounds.midX, y: bounds.maxY - 1), CGVector(dx: 0, dy: 30), [false, false, false, true]),
+ ("bottom", NSPoint(x: bounds.midX, y: 1), CGVector(dx: 0, dy: -30), [false, false, true, false]),
+ ("left", NSPoint(x: 1, y: bounds.midY), CGVector(dx: -30, dy: 0), [true, false, false, false]),
+ ("right", NSPoint(x: bounds.maxX - 1, y: bounds.midY), CGVector(dx: 30, dy: 0), [false, true, false, false]),
+ ("top left", NSPoint(x: 1, y: bounds.maxY - 1), CGVector(dx: -30, dy: 30), [true, false, false, true]),
+ ("top right", NSPoint(x: bounds.maxX - 1, y: bounds.maxY - 1), CGVector(dx: 30, dy: 30), [false, true, false, true]),
+ ]
+ for (edge, point, by, moves) in cases {
+ window.setFrame(start, display: true)
+ pump(0.1)
+ // AppKit's resize reads the events that follow the press itself,
+ // so they are queued first.
+ let from = window.convertPoint(toScreen: point)
+ let to = window.convertPoint(fromScreen: NSPoint(x: from.x + by.dx, y: from.y + by.dy))
+ NSApp.postEvent(event(.leftMouseDragged, at: to), atStart: false)
+ NSApp.postEvent(event(.leftMouseUp, at: to), atStart: false)
+ NSApp.sendEvent(event(.leftMouseDown, at: point))
+ pump(0.1)
+ let now = window.frame
+ let moved = [now.minX != start.minX, now.maxX != start.maxX, now.minY != start.minY, now.maxY != start.maxY]
+ XCTAssertEqual(moved, moves, "\(edge): \(start) became \(now)")
+ XCTAssertGreaterThan(now.width * now.height, start.width * start.height, "\(edge) grew")
+ }
+ }
+
+ func testDraggingThePictureMovesTheWindow() {
+ open()
+ let moved = drag(from: NSPoint(x: bounds.midX, y: bounds.midY), by: CGVector(dx: 40, dy: -25))
+ XCTAssertEqual(moved, CGVector(dx: 40, dy: -25))
+ XCTAssertEqual(window.frame.size, start.size)
+ let again = drag(from: NSPoint(x: 150, y: 300), by: CGVector(dx: -30, dy: 20))
+ XCTAssertEqual(again, CGVector(dx: -30, dy: 20), "anywhere on it")
+ }
+
+ func testAClickOnThePictureDoesNotMoveTheWindow() {
+ open()
+ let moved = drag(from: NSPoint(x: bounds.midX, y: bounds.midY), by: CGVector(dx: 2, dy: 1))
+ XCTAssertEqual(moved, CGVector(dx: 0, dy: 0))
+ }
+
+ /// A picture arriving is an AppKit view over the tile, which took
+ /// the press for itself before 0.3.7, so the stage's gesture never
+ /// saw it. Behind another app's window too.
+ func testDraggingLiveVideoMovesTheWindow() {
+ let factory = RTCPeerConnectionFactory()
+ let track = factory.videoTrack(with: factory.videoSource(), trackId: "window-move")
+ window = NSWindow(
+ contentRect: start, styleMask: [.titled, .closable, .resizable], backing: .buffered,
+ defer: false)
+ window.isReleasedWhenClosed = false
+ window.contentView = NSHostingView(
+ rootView: MacVideoTile(track: track, label: "Ana", cornerRadius: 0, compact: true)
+ .movesWindow())
+ window.orderFrontRegardless()
+ pump(0.8)
+ let point = NSPoint(x: bounds.midX, y: bounds.midY)
+ XCTAssertFalse(
+ window.contentView!.superview!.hitTest(point) is RTCMTLNSVideoView, "the picture takes no clicks")
+ XCTAssertEqual(drag(from: point, by: CGVector(dx: 40, dy: -25)), CGVector(dx: 40, dy: -25))
+ }
+
+ /// The self view drags itself to a corner (a gesture of its own, which
+ /// the picture's does not take from it), and the window stays put.
+ func testTheSelfViewIsNotAHandle() {
+ open()
+ let moved = drag(from: NSPoint(x: bounds.maxX - 100, y: 80), by: CGVector(dx: -750, dy: 450))
+ XCTAssertEqual(moved, CGVector(dx: 0, dy: 0))
+ }
+
+ func testTheBarIsNotAHandle() {
+ open(controls: true)
+ let moved = drag(from: NSPoint(x: bounds.midX, y: 40), by: CGVector(dx: 30, dy: 30))
+ XCTAssertEqual(moved, CGVector(dx: 0, dy: 0))
+ }
+ }
+#endif