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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Private simulator behavior is implemented locally in:
- Accessibility bridge: `cli/XCWAccessibilityBridge.*`

The current repo uses the private boot path, private display bridge, and private accessibility translation bridge directly. The browser streams frames from that bridge, injects touch and keyboard events through the same native session layer, inspects accessibility through `AccessibilityPlatformTranslation`, and renders device chrome from `cli/XCWChromeRenderer.*`.
Physical chrome button support uses DeviceKit `chrome.json` input geometry for browser hit targets. Volume, action, and mute buttons dispatch through `IndigoHIDMessageForHIDArbitrary` with consumer/telephony HID usage pairs from the device chrome metadata; home, lock, and app-switcher remain on the existing SimulatorKit button paths.
Physical chrome button support uses DeviceKit `chrome.json` input geometry for browser hit targets. Volume, action, mute, Apple Watch digital crown, Watch side button, and Watch left-side button dispatch through `IndigoHIDMessageForHIDArbitrary` with consumer/telephony/vendor HID usage pairs from the device chrome metadata; home, lock, and app-switcher remain on the existing SimulatorKit button paths. Apple Watch Digital Crown rotation dispatches through `IndigoHIDMessageForScrollEvent` with the same digitizer target as touch input.
WebKit inspection uses the simulator `webinspectord` Unix socket named `com.apple.webinspectord_sim.socket` and WebKit's binary-plist Remote Inspector selectors. It lists only WebKit content that the runtime exposes as inspectable. For app-owned `WKWebView` on iOS 16.4 and newer, the app must set `isInspectable = true`.

## Build and Run
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ simdeck type <udid> --file message.txt
simdeck button <udid> lock --duration-ms 1000
simdeck button <udid> volume-up
simdeck button <udid> action --duration-ms 1000
simdeck button <udid> digital-crown
simdeck crown <udid> --delta 50
simdeck button <udid> left-side-button
simdeck batch <udid> --step "tap --label Continue" --step "type 'hello'" --step "wait-for --label hello"
simdeck dismiss-keyboard <udid>
simdeck home <udid>
Expand Down
2 changes: 2 additions & 0 deletions cli/DFPrivateSimulatorDisplayBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ NS_SWIFT_NAME(PrivateSimulatorDisplayBridge)
usagePage:(nullable NSNumber *)usagePage
usage:(nullable NSNumber *)usage
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(sendHardwareButton(named:pressed:usagePage:usage:));
- (BOOL)rotateDigitalCrownByDelta:(double)delta
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(rotateDigitalCrown(delta:));

- (BOOL)rotateRight:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(rotateRight());
- (BOOL)rotateLeft:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(rotateLeft());
Expand Down
78 changes: 78 additions & 0 deletions cli/DFPrivateSimulatorDisplayBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
typedef IndigoHIDMessage *(*DFIndigoHIDMessageForKeyboardNSEventFn)(NSEvent *event);
typedef IndigoHIDMessage *(*DFIndigoHIDMessageForButtonFn)(uint32_t buttonCode, uint32_t operation, uint32_t target);
typedef IndigoHIDMessage *(*DFIndigoHIDMessageForHIDArbitraryFn)(uint32_t target, uint32_t page, uint32_t usage, uint32_t operation);
typedef IndigoHIDMessage *(*DFIndigoHIDMessageForScrollEventFn)(uint32_t target, double deltaX, double deltaY, double momentumPhase);
typedef IndigoHIDMessage *(*DFIndigoHIDServiceMessageFn)(void);

#pragma pack(push, 4)
Expand Down Expand Up @@ -1431,6 +1432,29 @@ static void DFWarmIndigoHIDServices(id hidClient) {
return message;
}

static IndigoHIDMessage *DFCreateScrollHIDMessage(uint32_t target, double deltaX, double deltaY, NSError **error) {
DFIndigoHIDMessageForScrollEventFn scrollMessage = (DFIndigoHIDMessageForScrollEventFn)dlsym(RTLD_DEFAULT, "IndigoHIDMessageForScrollEvent");
if (scrollMessage == NULL) {
if (error != NULL) {
*error = DFMakeError(
DFPrivateSimulatorErrorCodeTouchDispatchFailed,
@"SimulatorKit did not expose IndigoHIDMessageForScrollEvent."
);
}
return NULL;
}

IndigoHIDMessage *message = scrollMessage(target, deltaX, deltaY, 0);
if (message == NULL && error != NULL) {
*error = DFMakeError(
DFPrivateSimulatorErrorCodeTouchDispatchFailed,
[NSString stringWithFormat:@"SimulatorKit could not construct scroll HID for delta %.3f.", deltaY]
);
}

return message;
}

static BOOL DFCallSwiftUnitAngleMeasurementGetterByFunction(id selfObject, void *function, DFUnitAngleMeasurement *measurement) {
if (selfObject == nil || function == NULL || measurement == NULL) {
return NO;
Expand Down Expand Up @@ -3415,6 +3439,9 @@ - (BOOL)sendHardwareButtonNamed:(NSString *)buttonName
@"volume-down": @[ @(DFConsumerControlUsagePage), @234 ],
@"action": @[ @(0x0b), @45 ],
@"mute": @[ @(0x0b), @46 ],
@"digital-crown": @[ @(DFConsumerControlUsagePage), @64 ],
@"side-button": @[ @(DFConsumerControlUsagePage), @149 ],
@"left-side-button": @[ @(0xff01), @512 ],
};
});

Expand Down Expand Up @@ -3494,6 +3521,57 @@ - (BOOL)sendHardwareButtonNamed:(NSString *)buttonName
return success;
}

- (BOOL)rotateDigitalCrownByDelta:(double)delta
error:(NSError * _Nullable __autoreleasing *)error {
if (!isfinite(delta)) {
if (error != NULL) {
*error = DFMakeError(
DFPrivateSimulatorErrorCodeTouchDispatchFailed,
@"Digital Crown delta must be finite."
);
}
return NO;
}

__block BOOL success = NO;
__block NSError *dispatchError = nil;

dispatch_block_t work = ^{
if (self->_hidClient == nil) {
dispatchError = DFMakeError(
DFPrivateSimulatorErrorCodeTouchDispatchFailed,
@"SimulatorKit did not provide a headless HID client for Digital Crown rotation."
);
return;
}

NSError *messageError = nil;
IndigoHIDMessage *message = DFCreateScrollHIDMessage(DFIndigoTouchTarget, 0, delta, &messageError);
if (message == NULL || !DFSendHIDMessage(self->_hidClient, message, YES, &messageError)) {
dispatchError = messageError;
return;
}

DFLog(@"Sending Digital Crown rotation delta=%.3f", delta);
success = YES;
};

if (dispatch_get_specific(DFPrivateSimulatorCallbackQueueKey) != NULL) {
work();
} else {
dispatch_sync(_callbackQueue, work);
}

if (!success && error != NULL) {
*error = dispatchError ?: DFMakeError(
DFPrivateSimulatorErrorCodeTouchDispatchFailed,
@"SimulatorKit rejected Digital Crown rotation."
);
}

return success;
}

- (BOOL)rotateRight:(NSError * _Nullable __autoreleasing *)error {
return [self rotateByDegrees:90.0 error:error];
}
Expand Down
87 changes: 63 additions & 24 deletions cli/XCWChromeRenderer.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ @interface XCWChromeRenderer ()
+ (nullable NSDictionary *)inputNamed:(NSString *)buttonName
chromeInfo:(NSDictionary *)chromeInfo
error:(NSError * _Nullable __autoreleasing *)error;
+ (NSEdgeInsets)devicePaddingForChromeInfo:(NSDictionary *)chromeInfo;
@end

@implementation XCWChromeRenderer
Expand Down Expand Up @@ -665,6 +666,14 @@ + (BOOL)drawInputImagesForChromeInfo:(NSDictionary *)chromeInfo

+ (CGRect)fullFrameForChromeInfo:(NSDictionary *)chromeInfo
chromeSize:(CGSize)chromeSize {
NSEdgeInsets padding = [self devicePaddingForChromeInfo:chromeInfo];
if (padding.top != 0.0 || padding.left != 0.0 || padding.bottom != 0.0 || padding.right != 0.0) {
return CGRectMake(-padding.left,
-padding.top,
chromeSize.width + padding.left + padding.right,
chromeSize.height + padding.top + padding.bottom);
}

CGRect bounds = CGRectMake(0.0, 0.0, chromeSize.width, chromeSize.height);
NSDictionary *json = chromeInfo[@"json"];
NSString *chromePath = chromeInfo[@"chromePath"];
Expand Down Expand Up @@ -712,55 +721,85 @@ + (CGRect)inputFrameForInput:(NSDictionary *)input
inSize:(CGSize)size
offsetName:(NSString *)offsetName {
NSDictionary *offsets = [input[@"offsets"] isKindOfClass:[NSDictionary class]] ? input[@"offsets"] : @{};
NSDictionary *requestedOffset = [offsets[offsetName] isKindOfClass:[NSDictionary class]] ? offsets[offsetName] : nil;
NSDictionary *normalOffset = [offsets[@"normal"] isKindOfClass:[NSDictionary class]] ? offsets[@"normal"] : nil;
NSDictionary *rolloverOffset = [offsets[@"rollover"] isKindOfClass:[NSDictionary class]] ? offsets[@"rollover"] : nil;
NSDictionary *offset = requestedOffset ?: rolloverOffset ?: normalOffset ?: @{};
CGFloat offsetX = [self numberValue:offset[@"x"]];
CGFloat offsetY = [self numberValue:offset[@"y"]];
NSDictionary *requestedOffset = [offsets[offsetName] isKindOfClass:[NSDictionary class]] ? offsets[offsetName] : nil;
NSDictionary *primaryOffset = normalOffset ?: rolloverOffset ?: requestedOffset ?: @{};
NSDictionary *secondaryOffset = rolloverOffset ?: normalOffset ?: requestedOffset ?: @{};
CGFloat normalX = [self numberValue:primaryOffset[@"x"]];
CGFloat normalY = [self numberValue:primaryOffset[@"y"]];
CGFloat rolloverX = [self numberValue:secondaryOffset[@"x"]];
CGFloat rolloverY = [self numberValue:secondaryOffset[@"y"]];
CGFloat restX = normalX;
CGFloat restY = normalY;
NSString *anchor = [input[@"anchor"] isKindOfClass:[NSString class]] ? input[@"anchor"] : @"";
NSString *align = [input[@"align"] isKindOfClass:[NSString class]] ? input[@"align"] : @"";

CGFloat x = offsetX - (assetSize.width / 2.0);
CGFloat y = offsetY;
if ([offsetName isEqualToString:@"rollover"]) {
restX = rolloverX;
restY = rolloverY;
} else if ([anchor isEqualToString:@"left"]) {
restX = rolloverX;
restY = rolloverY;
} else if ([anchor isEqualToString:@"right"] ||
[anchor isEqualToString:@"top"] ||
[anchor isEqualToString:@"bottom"]) {
restX = (2.0 * normalX) - rolloverX;
restY = (2.0 * normalY) - rolloverY;
}

CGFloat x = restX - (assetSize.width / 2.0);
CGFloat y = restY;
if ([anchor isEqualToString:@"left"]) {
x = offsetX - (assetSize.width / 2.0);
x = restX - (assetSize.width / 2.0);
} else if ([anchor isEqualToString:@"right"]) {
x = size.width + offsetX - (assetSize.width / 2.0);
x = size.width + restX;
y = restY;
} else if ([anchor isEqualToString:@"top"]) {
y = offsetY;
if ([align isEqualToString:@"trailing"]) {
x = size.width + restX - assetSize.width;
} else {
x = restX;
}
y = restY - assetSize.height;
} else if ([anchor isEqualToString:@"bottom"]) {
y = size.height + offsetY;
if ([align isEqualToString:@"trailing"]) {
x = size.width + restX - assetSize.width;
} else {
x = restX;
}
y = size.height + restY;
}

if ([anchor isEqualToString:@"left"] || [anchor isEqualToString:@"right"]) {
if ([align isEqualToString:@"center"]) {
y = (size.height - assetSize.height) / 2.0 + offsetY;
y = (size.height - assetSize.height) / 2.0 + restY;
} else if ([align isEqualToString:@"trailing"]) {
y = size.height - assetSize.height + offsetY;
y = size.height - assetSize.height + restY;
}
} else if ([anchor isEqualToString:@"top"] || [anchor isEqualToString:@"bottom"]) {
CGFloat baseX = 0.0;
if ([align isEqualToString:@"center"]) {
baseX = size.width / 2.0;
} else if ([align isEqualToString:@"trailing"]) {
baseX = size.width;
}
x = baseX + offsetX - (assetSize.width / 2.0);
if ([align isEqualToString:@"center"]) {
x = (size.width / 2.0) + offsetX - (assetSize.width / 2.0);
} else if ([align isEqualToString:@"trailing"]) {
x = size.width + offsetX - (assetSize.width / 2.0);
x = (size.width / 2.0) + restX - (assetSize.width / 2.0);
}
} else if ([align isEqualToString:@"center"]) {
x = (size.width - assetSize.width) / 2.0 + offsetX;
x = (size.width - assetSize.width) / 2.0 + restX;
} else if ([align isEqualToString:@"trailing"]) {
x = size.width - assetSize.width + offsetX;
x = size.width - assetSize.width + restX;
}

return CGRectMake(x, y, assetSize.width, assetSize.height);
}

+ (NSEdgeInsets)devicePaddingForChromeInfo:(NSDictionary *)chromeInfo {
NSDictionary *json = chromeInfo[@"json"];
NSDictionary *images = [json[@"images"] isKindOfClass:[NSDictionary class]] ? json[@"images"] : @{};
NSDictionary *padding = [images[@"devicePadding"] isKindOfClass:[NSDictionary class]] ? images[@"devicePadding"] : @{};
return NSEdgeInsetsMake([self numberValue:padding[@"top"]],
[self numberValue:padding[@"left"]],
[self numberValue:padding[@"bottom"]],
[self numberValue:padding[@"right"]]);
}

+ (NSArray<NSDictionary<NSString *, id> *> *)buttonProfilesForChromeInfo:(NSDictionary *)chromeInfo
chromeSize:(CGSize)chromeSize
chromeOffset:(CGPoint)chromeOffset {
Expand Down
2 changes: 2 additions & 0 deletions cli/XCWPrivateSimulatorSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ typedef void (^XCWPrivateSimulatorEncodedFrameHandler)(NSData *sampleData,
usagePage:(nullable NSNumber *)usagePage
usage:(nullable NSNumber *)usage
error:(NSError * _Nullable * _Nullable)error;
- (BOOL)rotateDigitalCrownByDelta:(double)delta
error:(NSError * _Nullable * _Nullable)error;
- (BOOL)openAppSwitcher:(NSError * _Nullable * _Nullable)error;
- (BOOL)rotateRight:(NSError * _Nullable * _Nullable)error;
- (BOOL)rotateLeft:(NSError * _Nullable * _Nullable)error;
Expand Down
5 changes: 5 additions & 0 deletions cli/XCWPrivateSimulatorSession.m
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,11 @@ - (BOOL)sendHardwareButtonNamed:(NSString *)buttonName
error:error];
}

- (BOOL)rotateDigitalCrownByDelta:(double)delta
error:(NSError * _Nullable __autoreleasing *)error {
return [_displayBridge rotateDigitalCrownByDelta:delta error:error];
}

- (BOOL)openAppSwitcher:(NSError * _Nullable __autoreleasing *)error {
return [_displayBridge openAppSwitcher:error];
}
Expand Down
2 changes: 2 additions & 0 deletions cli/native/XCWNativeBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ bool xcw_native_press_home(const char * _Nonnull udid, char * _Nullable * _Nulla
bool xcw_native_open_app_switcher(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
bool xcw_native_press_button(const char * _Nonnull udid, const char * _Nonnull button_name, uint32_t duration_ms, char * _Nullable * _Nullable error_message);
bool xcw_native_send_button(const char * _Nonnull udid, const char * _Nonnull button_name, bool pressed, bool has_usage, uint32_t usage_page, uint32_t usage, char * _Nullable * _Nullable error_message);
bool xcw_native_rotate_crown(const char * _Nonnull udid, double delta, char * _Nullable * _Nullable error_message);
bool xcw_native_rotate_right(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
bool xcw_native_rotate_left(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
bool xcw_native_erase_simulator(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
Expand Down Expand Up @@ -84,6 +85,7 @@ bool xcw_native_session_send_key(void * _Nonnull handle, uint16_t key_code, uint
bool xcw_native_session_press_home(void * _Nonnull handle, char * _Nullable * _Nullable error_message);
bool xcw_native_session_press_button(void * _Nonnull handle, const char * _Nonnull button_name, uint32_t duration_ms, char * _Nullable * _Nullable error_message);
bool xcw_native_session_send_button(void * _Nonnull handle, const char * _Nonnull button_name, bool pressed, bool has_usage, uint32_t usage_page, uint32_t usage, char * _Nullable * _Nullable error_message);
bool xcw_native_session_rotate_crown(void * _Nonnull handle, double delta, char * _Nullable * _Nullable error_message);
bool xcw_native_session_open_app_switcher(void * _Nonnull handle, char * _Nullable * _Nullable error_message);
bool xcw_native_session_rotate_right(void * _Nonnull handle, char * _Nullable * _Nullable error_message);
bool xcw_native_session_rotate_left(void * _Nonnull handle, char * _Nullable * _Nullable error_message);
Expand Down
27 changes: 27 additions & 0 deletions cli/native/XCWNativeBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,22 @@ bool xcw_native_send_button(const char *udid, const char *button_name, bool pres
}
}

bool xcw_native_rotate_crown(const char *udid, double delta, char **error_message) {
@autoreleasepool {
DFPrivateSimulatorDisplayBridge *bridge = XCWInputBridgeForUDID(udid, error_message);
if (bridge == nil) {
return false;
}
NSError *error = nil;
BOOL ok = [bridge rotateDigitalCrownByDelta:delta error:&error];
[bridge disconnect];
if (!ok) {
XCWSetErrorMessage(error_message, error);
}
return ok;
}
}

bool xcw_native_rotate_right(const char *udid, char **error_message) {
@autoreleasepool {
DFPrivateSimulatorDisplayBridge *bridge = XCWInputBridgeForUDID(udid, error_message);
Expand Down Expand Up @@ -1032,6 +1048,17 @@ bool xcw_native_session_send_button(void *handle, const char *button_name, bool
}
}

bool xcw_native_session_rotate_crown(void *handle, double delta, char **error_message) {
@autoreleasepool {
NSError *error = nil;
BOOL ok = [XCWNativeSessionFromHandle(handle) rotateDigitalCrownByDelta:delta error:&error];
if (!ok) {
XCWSetErrorMessage(error_message, error);
}
return ok;
}
}

bool xcw_native_session_open_app_switcher(void *handle, char **error_message) {
@autoreleasepool {
NSError *error = nil;
Expand Down
2 changes: 2 additions & 0 deletions cli/native/XCWNativeSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ NS_ASSUME_NONNULL_BEGIN
usagePage:(nullable NSNumber *)usagePage
usage:(nullable NSNumber *)usage
error:(NSError * _Nullable * _Nullable)error;
- (BOOL)rotateDigitalCrownByDelta:(double)delta
error:(NSError * _Nullable * _Nullable)error;
- (BOOL)openAppSwitcher:(NSError * _Nullable * _Nullable)error;
- (BOOL)rotateRight:(NSError * _Nullable * _Nullable)error;
- (BOOL)rotateLeft:(NSError * _Nullable * _Nullable)error;
Expand Down
5 changes: 5 additions & 0 deletions cli/native/XCWNativeSession.m
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ - (BOOL)sendHardwareButtonNamed:(NSString *)buttonName
error:error];
}

- (BOOL)rotateDigitalCrownByDelta:(double)delta
error:(NSError * _Nullable __autoreleasing *)error {
return [self.session rotateDigitalCrownByDelta:delta error:error];
}

- (BOOL)openAppSwitcher:(NSError * _Nullable __autoreleasing *)error {
return [self.session openAppSwitcher:error];
}
Expand Down
Loading
Loading