Skip to content

Commit 9d6155a

Browse files
authored
Refine Watch chrome screen masking and radius handling (#26)
1 parent e6e2b61 commit 9d6155a

4 files changed

Lines changed: 261 additions & 58 deletions

File tree

cli/XCWChromeRenderer.m

Lines changed: 183 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ + (nullable NSData *)PNGDataForDeviceName:(NSString *)deviceName
106106
}
107107
}
108108
CGContextTranslateCTM(context, -chromeX, -chromeY);
109-
[self clearScreenAreaForProfile:profile context:context];
109+
if (![self clearScreenAreaForChromeInfo:chromeInfo profile:profile context:context error:error]) {
110+
CGContextRestoreGState(context);
111+
CGContextRelease(context);
112+
return nil;
113+
}
110114
if (![self drawSensorBarForChromeInfo:chromeInfo profile:profile context:context error:error]) {
111115
CGContextRestoreGState(context);
112116
CGContextRelease(context);
@@ -237,8 +241,11 @@ + (nullable NSData *)screenMaskPNGDataForDeviceName:(NSString *)deviceName
237241
CGFloat screenScale = MAX([self numberValue:plist[@"mainScreenScale"]], 1.0);
238242
CGFloat profileScreenWidth = [self numberValue:plist[@"mainScreenWidth"]];
239243
CGFloat profileScreenHeight = [self numberValue:plist[@"mainScreenHeight"]];
240-
CGFloat pointScreenWidth = watchProfile ? profileScreenWidth : profileScreenWidth / screenScale;
241-
CGFloat pointScreenHeight = watchProfile ? profileScreenHeight : profileScreenHeight / screenScale;
244+
CGSize profileScreenSize = [self screenSizeForChromeInfo:chromeInfo
245+
chromeSize:compositeSize
246+
screenScale:screenScale];
247+
CGFloat pointScreenWidth = profileScreenSize.width;
248+
CGFloat pointScreenHeight = profileScreenSize.height;
242249

243250
CGFloat screenWidth;
244251
CGFloat screenHeight;
@@ -262,15 +269,10 @@ + (nullable NSData *)screenMaskPNGDataForDeviceName:(NSString *)deviceName
262269
screenHeight = MAX(compositeSize.height - standHeight - bezelTop - bezelBottom, 1.0);
263270
}
264271

265-
CGFloat innerRadius = MAX(rawCornerRadius - MAX(bezelLeft, bezelTop), 0.0);
272+
CGFloat innerRadius = MAX(rawCornerRadius - MAX(screenX, screenY), 0.0);
266273
CGFloat radiusScale = pointScreenWidth > 0.0 ? screenWidth / pointScreenWidth : 1.0;
267-
CGFloat chromeCornerRadius = watchProfile ? rawCornerRadius : innerRadius * radiusScale;
274+
CGFloat chromeCornerRadius = innerRadius * radiusScale;
268275
CGFloat cornerRadius = chromeCornerRadius;
269-
CGFloat maskCornerRadius = [self framebufferMaskCornerRadiusForChromeInfo:chromeInfo
270-
pointScreenWidth:pointScreenWidth];
271-
if (!phoneProfile && maskCornerRadius > 0.0) {
272-
cornerRadius = maskCornerRadius * radiusScale;
273-
}
274276

275277
CGRect fullFrame = [self fullFrameForChromeInfo:chromeInfo chromeSize:compositeSize];
276278
CGFloat chromeX = -CGRectGetMinX(fullFrame);
@@ -388,13 +390,11 @@ + (CGSize)compositeSizeForChromeInfo:(NSDictionary *)chromeInfo
388390
NSDictionary *bord = [paths[@"simpleOutsideBorder"] isKindOfClass:[NSDictionary class]] ? paths[@"simpleOutsideBorder"] : @{};
389391
NSDictionary *bordI = [bord[@"insets"] isKindOfClass:[NSDictionary class]] ? bord[@"insets"] : @{};
390392
CGFloat screenScale = MAX([self numberValue:plist[@"mainScreenScale"]], 1.0);
391-
BOOL watchProfile = [self isWatchProfile:plist];
392-
CGFloat screenWidth = [self numberValue:plist[@"mainScreenWidth"]];
393-
CGFloat screenHeight = [self numberValue:plist[@"mainScreenHeight"]];
394-
if (!watchProfile) {
395-
screenWidth /= screenScale;
396-
screenHeight /= screenScale;
397-
}
393+
CGSize screenSize = [self screenSizeForChromeInfo:chromeInfo
394+
chromeSize:CGSizeZero
395+
screenScale:screenScale];
396+
CGFloat screenWidth = screenSize.width;
397+
CGFloat screenHeight = screenSize.height;
398398
CGFloat bezelLeft = [self numberValue:sizing[@"leftWidth"]] + [self numberValue:bordI[@"left"]];
399399
CGFloat bezelRight = [self numberValue:sizing[@"rightWidth"]] + [self numberValue:bordI[@"right"]];
400400
CGFloat bezelTop = [self numberValue:sizing[@"topHeight"]] + [self numberValue:bordI[@"top"]];
@@ -496,7 +496,7 @@ + (BOOL)drawSlicedChromeInfo:(NSDictionary *)chromeInfo
496496
if (NSWidth(nsRect) <= 0.0 || NSHeight(nsRect) <= 0.0) {
497497
continue;
498498
}
499-
if ([self drawPDFAtPath:assetPath inRect:NSRectToCGRect(nsRect) context:context error:error]) {
499+
if ([self drawRasterizedPDFAtPath:assetPath inRect:NSRectToCGRect(nsRect) context:context error:error]) {
500500
drewAny = YES;
501501
} else {
502502
return NO;
@@ -549,26 +549,26 @@ + (BOOL)drawStandImagesForChromeInfo:(NSDictionary *)chromeInfo
549549
CGFloat y = chromeYMax;
550550

551551
if (leftPath.length > 0 && leftWidth > 0.0) {
552-
if (![self drawPDFAtPath:leftPath
553-
inRect:CGRectMake(x, y, leftWidth, standHeight)
554-
context:context
555-
error:error]) {
552+
if (![self drawRasterizedPDFAtPath:leftPath
553+
inRect:CGRectMake(x, y, leftWidth, standHeight)
554+
context:context
555+
error:error]) {
556556
return NO;
557557
}
558558
}
559559
if (centerPath.length > 0) {
560-
if (![self drawPDFAtPath:centerPath
561-
inRect:CGRectMake(x + leftWidth, y, centerWidth, standHeight)
562-
context:context
563-
error:error]) {
560+
if (![self drawRasterizedPDFAtPath:centerPath
561+
inRect:CGRectMake(x + leftWidth, y, centerWidth, standHeight)
562+
context:context
563+
error:error]) {
564564
return NO;
565565
}
566566
}
567567
if (rightPath.length > 0 && rightWidth > 0.0) {
568-
if (![self drawPDFAtPath:rightPath
569-
inRect:CGRectMake(x + leftWidth + centerWidth, y, rightWidth, standHeight)
570-
context:context
571-
error:error]) {
568+
if (![self drawRasterizedPDFAtPath:rightPath
569+
inRect:CGRectMake(x + leftWidth + centerWidth, y, rightWidth, standHeight)
570+
context:context
571+
error:error]) {
572572
return NO;
573573
}
574574
}
@@ -654,17 +654,13 @@ + (CGRect)inputFrameForInput:(NSDictionary *)input
654654
CGFloat x = offsetX;
655655
CGFloat y = offsetY;
656656
if ([anchor isEqualToString:@"left"]) {
657-
CGFloat visibleWidth = MAX(assetSize.width - MAX(offsetX, 0.0), 0.0) / 2.0;
658-
x = -visibleWidth;
657+
x = offsetX - (assetSize.width / 2.0);
659658
} else if ([anchor isEqualToString:@"right"]) {
660-
CGFloat visibleWidth = MAX(assetSize.width + MIN(offsetX, 0.0), 0.0) / 2.0;
661-
x = size.width - assetSize.width + visibleWidth;
659+
x = size.width + offsetX - (assetSize.width / 2.0);
662660
} else if ([anchor isEqualToString:@"top"]) {
663-
CGFloat visibleHeight = MAX(assetSize.height - MAX(offsetY, 0.0), 0.0) / 2.0;
664-
y = -visibleHeight;
661+
y = offsetY;
665662
} else if ([anchor isEqualToString:@"bottom"]) {
666-
CGFloat visibleHeight = MAX(assetSize.height + MIN(offsetY, 0.0), 0.0) / 2.0;
667-
y = size.height - assetSize.height + visibleHeight;
663+
y = size.height + offsetY;
668664
}
669665

670666
if ([anchor isEqualToString:@"left"] || [anchor isEqualToString:@"right"]) {
@@ -688,21 +684,162 @@ + (CGRect)inputFrameForInput:(NSDictionary *)input
688684
return CGRectMake(x, y, assetSize.width, assetSize.height);
689685
}
690686

691-
+ (void)clearScreenAreaForProfile:(NSDictionary *)profile
692-
context:(CGContextRef)context {
687+
+ (CGSize)screenSizeForChromeInfo:(NSDictionary *)chromeInfo
688+
chromeSize:(CGSize)chromeSize
689+
screenScale:(CGFloat)screenScale {
690+
NSDictionary *plist = chromeInfo[@"plist"];
691+
CGFloat rawWidth = [self numberValue:plist[@"mainScreenWidth"]];
692+
CGFloat rawHeight = [self numberValue:plist[@"mainScreenHeight"]];
693+
CGFloat scale = MAX(screenScale, 1.0);
694+
if (![self isWatchProfile:plist]) {
695+
return CGSizeMake(rawWidth / scale, rawHeight / scale);
696+
}
697+
698+
if (chromeSize.width > 0.0 &&
699+
chromeSize.height > 0.0 &&
700+
rawWidth <= chromeSize.width &&
701+
rawHeight <= chromeSize.height) {
702+
return CGSizeMake(rawWidth, rawHeight);
703+
}
704+
return CGSizeMake(rawWidth / scale, rawHeight / scale);
705+
}
706+
707+
+ (BOOL)drawRasterizedPDFAtPath:(NSString *)path
708+
inRect:(CGRect)rect
709+
context:(CGContextRef)context
710+
error:(NSError * _Nullable __autoreleasing *)error {
711+
CGImageRef image = [self newImageForPDFAtPath:path error:error];
712+
if (image == NULL) {
713+
return NO;
714+
}
715+
716+
CGFloat imageWidth = MAX((CGFloat)CGImageGetWidth(image), 1.0);
717+
CGFloat imageHeight = MAX((CGFloat)CGImageGetHeight(image), 1.0);
718+
CGContextSaveGState(context);
719+
CGContextClipToRect(context, rect);
720+
CGContextTranslateCTM(context, rect.origin.x, rect.origin.y + rect.size.height);
721+
CGContextScaleCTM(context, rect.size.width / imageWidth, -rect.size.height / imageHeight);
722+
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image);
723+
CGContextRestoreGState(context);
724+
CGImageRelease(image);
725+
return YES;
726+
}
727+
728+
+ (nullable CGImageRef)newImageForPDFAtPath:(NSString *)path
729+
error:(NSError * _Nullable __autoreleasing *)error CF_RETURNS_RETAINED {
730+
if (path.length == 0) {
731+
if (error != NULL) {
732+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
733+
code:14
734+
userInfo:@{
735+
NSLocalizedDescriptionKey: @"DeviceKit chrome asset path was empty.",
736+
}];
737+
}
738+
return NULL;
739+
}
740+
741+
CGPDFDocumentRef document = CGPDFDocumentCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path]);
742+
if (document == NULL) {
743+
if (error != NULL) {
744+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
745+
code:7
746+
userInfo:@{
747+
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to open DeviceKit chrome PDF %@.", path.lastPathComponent],
748+
}];
749+
}
750+
return NULL;
751+
}
752+
CGPDFPageRef page = CGPDFDocumentGetPage(document, 1);
753+
if (page == NULL) {
754+
CGPDFDocumentRelease(document);
755+
if (error != NULL) {
756+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
757+
code:8
758+
userInfo:@{
759+
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"DeviceKit chrome PDF %@ did not contain a renderable page.", path.lastPathComponent],
760+
}];
761+
}
762+
return NULL;
763+
}
764+
765+
CGRect mediaBox = CGPDFPageGetBoxRect(page, kCGPDFCropBox);
766+
if (CGRectIsEmpty(mediaBox)) {
767+
mediaBox = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
768+
}
769+
NSInteger width = MAX((NSInteger)ceil(mediaBox.size.width), 1);
770+
NSInteger height = MAX((NSInteger)ceil(mediaBox.size.height), 1);
771+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
772+
CGContextRef context = CGBitmapContextCreate(NULL,
773+
width,
774+
height,
775+
8,
776+
0,
777+
colorSpace,
778+
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
779+
CGColorSpaceRelease(colorSpace);
780+
if (context == NULL) {
781+
CGPDFDocumentRelease(document);
782+
if (error != NULL) {
783+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
784+
code:9
785+
userInfo:@{
786+
NSLocalizedDescriptionKey: @"Unable to create a CoreGraphics bitmap context for DeviceKit chrome asset rendering.",
787+
}];
788+
}
789+
return NULL;
790+
}
791+
792+
CGContextClearRect(context, CGRectMake(0, 0, width, height));
793+
CGContextTranslateCTM(context, -mediaBox.origin.x, -mediaBox.origin.y);
794+
CGContextDrawPDFPage(context, page);
795+
CGImageRef image = CGBitmapContextCreateImage(context);
796+
CGContextRelease(context);
797+
CGPDFDocumentRelease(document);
798+
if (image == NULL && error != NULL) {
799+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
800+
code:10
801+
userInfo:@{
802+
NSLocalizedDescriptionKey: @"Unable to create a CGImage from the DeviceKit chrome asset.",
803+
}];
804+
}
805+
return image;
806+
}
807+
808+
+ (BOOL)clearScreenAreaForChromeInfo:(NSDictionary *)chromeInfo
809+
profile:(NSDictionary *)profile
810+
context:(CGContextRef)context
811+
error:(NSError * _Nullable __autoreleasing *)error {
693812
CGFloat x = [self numberValue:profile[@"screenX"]];
694813
CGFloat y = [self numberValue:profile[@"screenY"]];
695814
CGFloat width = [self numberValue:profile[@"screenWidth"]];
696815
CGFloat height = [self numberValue:profile[@"screenHeight"]];
697-
CGFloat radius = [self numberValue:profile[@"chromeCornerRadius"]];
698-
if (radius <= 0.0) {
699-
radius = [self numberValue:profile[@"cornerRadius"]];
700-
}
701816
if (width <= 0.0 || height <= 0.0) {
702-
return;
817+
return YES;
703818
}
704819

705820
CGRect rect = CGRectMake(x, y, width, height);
821+
BOOL hasScreenMask = [profile[@"hasScreenMask"] respondsToSelector:@selector(boolValue)] && [profile[@"hasScreenMask"] boolValue];
822+
if (hasScreenMask) {
823+
NSString *maskPath = [self screenMaskPathForChromeInfo:chromeInfo];
824+
if (maskPath.length > 0) {
825+
CGImageRef maskImage = [self newImageForPDFAtPath:maskPath error:error];
826+
if (maskImage == NULL) {
827+
return NO;
828+
}
829+
CGContextSaveGState(context);
830+
CGContextClipToMask(context, rect, maskImage);
831+
CGContextSetBlendMode(context, kCGBlendModeClear);
832+
CGContextFillRect(context, rect);
833+
CGContextRestoreGState(context);
834+
CGImageRelease(maskImage);
835+
return YES;
836+
}
837+
}
838+
839+
CGFloat radius = [self numberValue:profile[@"cornerRadius"]];
840+
if (radius <= 0.0) {
841+
radius = [self numberValue:profile[@"chromeCornerRadius"]];
842+
}
706843
CGFloat clampedRadius = MIN(MAX(radius, 0.0), MIN(width, height) / 2.0);
707844

708845
CGContextSaveGState(context);
@@ -726,6 +863,7 @@ + (void)clearScreenAreaForProfile:(NSDictionary *)profile
726863
CGPathRelease(path);
727864
}
728865
CGContextRestoreGState(context);
866+
return YES;
729867
}
730868

731869
+ (BOOL)drawSensorBarForChromeInfo:(NSDictionary *)chromeInfo

client/src/app/AppShell.tsx

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ import { AccessibilityInspector } from "../features/accessibility/AccessibilityI
4343
import { isEditableTarget } from "../features/input/keycodes";
4444
import { useKeyboardInput } from "../features/input/useKeyboardInput";
4545
import { usePointerInput } from "../features/input/usePointerInput";
46-
import { simulatorRuntimeLabel } from "../features/simulators/simulatorDisplay";
46+
import {
47+
shouldRenderNativeChrome,
48+
simulatorRuntimeLabel,
49+
} from "../features/simulators/simulatorDisplay";
4750
import { useSimulatorList } from "../features/simulators/useSimulatorList";
4851
import { sendWebRtcControlMessage } from "../features/stream/streamWorkerClient";
4952
import type {
@@ -163,17 +166,6 @@ function shouldUseRemoteStreamDefault(apiRoot: string): boolean {
163166
);
164167
}
165168

166-
function shouldRenderNativeChrome(simulator: SimulatorMetadata): boolean {
167-
const identifier = simulator.deviceTypeIdentifier ?? "";
168-
const name = simulator.name ?? "";
169-
return (
170-
identifier.includes(".iPhone-") ||
171-
identifier.includes(".iPad-") ||
172-
name.startsWith("iPhone") ||
173-
name.startsWith("iPad")
174-
);
175-
}
176-
177169
function simulatorDisplaySize(
178170
simulator: SimulatorMetadata | null,
179171
): Size | null {
@@ -1092,7 +1084,9 @@ export function AppShell({
10921084
top: `${(chromeScreenRect.y / viewportChromeProfile.totalHeight) * 100}%`,
10931085
width: `${(chromeScreenRect.width / viewportChromeProfile.totalWidth) * 100}%`,
10941086
height: `${(chromeScreenRect.height / viewportChromeProfile.totalHeight) * 100}%`,
1095-
borderRadius: chromeScreenBorderRadius ?? "0",
1087+
borderRadius: viewportChromeProfile.hasScreenMask
1088+
? "0"
1089+
: (chromeScreenBorderRadius ?? "0"),
10961090
...(viewportChromeProfile.hasScreenMask && selectedSimulator
10971091
? {
10981092
maskImage: `url("${buildScreenMaskUrl(

0 commit comments

Comments
 (0)