oficina / meet Public

Mac call: drag the picture to move the window, like FaceTime

Paul: "I should be able to click anywhere on the window to resize it."
Since 0.3.5 the full-window call has no title bar, and the 32 pt strip
along the top was the only place the window could be picked up by: a
drag on the picture did nothing. Resizing was never broken: AppKit keeps
the outer points of every edge and corner, the top included, above any
content, and the window stays .resizable. Tests now hold both.

- WindowMove: a drag anywhere on the stage (Focus, Grid, a shared
  screen, the fixtures' backdrops) moves the window once it has gone
  4 pt; less is a click, which still shows the controls. The bar,
  panels, notices, the self view and a tile's (i) keep their own
  clicks and drags. A double click on the picture does what the
  title-bar setting says, as the strip does. Not in full screen.
  The window is moved from its own mouse events in screen points,
  because SwiftUI's drag values shift as the window moves under them.
- The video view takes no clicks (hitTest nil), so a press on live
  video reaches SwiftUI's gestures, including the click that brings
  an inactive window forward (allowsWindowActivationEvents, macOS 15).
- TitleBarDoubleClick.perform(on:), shared by the strip and the stage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 718f30ef3713f3c192b3b117c9caa48379998225 parent 57d98cd
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