Skip to content

Commit f3a3a60

Browse files
committed
Use Android display corner radii
1 parent 292f8b1 commit f3a3a60

4 files changed

Lines changed: 163 additions & 23 deletions

File tree

client/src/api/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ export interface ChromeProfile {
113113
screenWidth: number;
114114
screenHeight: number;
115115
cornerRadius: number;
116+
cornerRadii?: {
117+
topLeft?: number;
118+
topRight?: number;
119+
bottomRight?: number;
120+
bottomLeft?: number;
121+
};
116122
chromeStyle?: "asset" | string;
117123
hasScreenMask?: boolean;
118124
buttons?: ChromeButtonProfile[];

client/src/app/AppShell.tsx

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,17 +1300,17 @@ export function AppShell({
13001300
} satisfies CSSProperties)
13011301
: null;
13021302
const screenOnlyStyle =
1303-
!viewportChromeProfile && isAndroidViewport
1304-
? null
1305-
: !viewportChromeProfile && chromeProfile && chromeProfile.screenWidth > 0
1306-
? ({
1303+
!viewportChromeProfile && chromeProfile && chromeProfile.screenWidth > 0
1304+
? isAndroidViewport
1305+
? androidScreenRadiusStyle(chromeProfile)
1306+
: ({
13071307
borderRadius: `${Math.min(
13081308
chromeProfile.cornerRadius *
13091309
(DEVICE_SCREEN_WIDTH / chromeProfile.screenWidth),
13101310
DEVICE_SCREEN_WIDTH / 2,
13111311
)}px`,
13121312
} satisfies CSSProperties)
1313-
: null;
1313+
: null;
13141314
const viewportScreenStyle = chromeScreenStyle ?? screenOnlyStyle;
13151315
const shellStyle = viewportChromeProfile
13161316
? {
@@ -2049,6 +2049,59 @@ export function AppShell({
20492049
);
20502050
}
20512051

2052+
function androidScreenRadiusStyle(
2053+
chromeProfile: ChromeProfile,
2054+
): CSSProperties | null {
2055+
if (chromeProfile.screenWidth <= 0) {
2056+
return null;
2057+
}
2058+
2059+
const scale = DEVICE_SCREEN_WIDTH / chromeProfile.screenWidth;
2060+
const maxRadius = DEVICE_SCREEN_WIDTH / 2;
2061+
const radii = chromeProfile.cornerRadii;
2062+
const topLeft = scaledScreenRadius(
2063+
radii?.topLeft ?? chromeProfile.cornerRadius,
2064+
scale,
2065+
maxRadius,
2066+
);
2067+
const topRight = scaledScreenRadius(
2068+
radii?.topRight ?? chromeProfile.cornerRadius,
2069+
scale,
2070+
maxRadius,
2071+
);
2072+
const bottomRight = scaledScreenRadius(
2073+
radii?.bottomRight ?? chromeProfile.cornerRadius,
2074+
scale,
2075+
maxRadius,
2076+
);
2077+
const bottomLeft = scaledScreenRadius(
2078+
radii?.bottomLeft ?? chromeProfile.cornerRadius,
2079+
scale,
2080+
maxRadius,
2081+
);
2082+
2083+
if (topLeft <= 0 && topRight <= 0 && bottomRight <= 0 && bottomLeft <= 0) {
2084+
return null;
2085+
}
2086+
2087+
const borderRadius = `${topLeft}px ${topRight}px ${bottomRight}px ${bottomLeft}px`;
2088+
return {
2089+
borderRadius,
2090+
borderTopLeftRadius: `${topLeft}px`,
2091+
borderTopRightRadius: `${topRight}px`,
2092+
borderBottomRightRadius: `${bottomRight}px`,
2093+
borderBottomLeftRadius: `${bottomLeft}px`,
2094+
clipPath: `inset(0 round ${borderRadius})`,
2095+
};
2096+
}
2097+
2098+
function scaledScreenRadius(radius: number, scale: number, maxRadius: number) {
2099+
if (!Number.isFinite(radius) || radius <= 0) {
2100+
return 0;
2101+
}
2102+
return Math.min(radius * scale, maxRadius);
2103+
}
2104+
20522105
function readDeviceQueryParam(): string | undefined {
20532106
if (typeof window === "undefined") {
20542107
return undefined;

client/src/styles/components.css

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,23 +1504,7 @@
15041504
}
15051505

15061506
.device-screen.android-screen {
1507-
--android-screen-radius: 18px;
1508-
--android-stream-overscan: 2px;
1509-
15101507
background: transparent;
1511-
border-radius: var(--android-screen-radius);
1512-
clip-path: inset(0 round var(--android-screen-radius));
1513-
}
1514-
1515-
.device-screen.android-screen .stream-canvas,
1516-
.device-screen.android-screen .stream-video {
1517-
inset: calc(-1 * var(--android-stream-overscan));
1518-
width: calc(
1519-
100% + var(--android-stream-overscan) + var(--android-stream-overscan)
1520-
);
1521-
height: calc(
1522-
100% + var(--android-stream-overscan) + var(--android-stream-overscan)
1523-
);
15241508
}
15251509

15261510
.stream-canvas {

server/src/android.rs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@ struct AndroidDisplayMetrics {
3535
width: f64,
3636
height: f64,
3737
rotation_quarter_turns: u16,
38+
corner_radii: AndroidCornerRadii,
39+
}
40+
41+
#[derive(Clone, Copy, Debug, PartialEq)]
42+
struct AndroidCornerRadii {
43+
top_left: f64,
44+
top_right: f64,
45+
bottom_right: f64,
46+
bottom_left: f64,
47+
}
48+
49+
impl AndroidCornerRadii {
50+
const ZERO: Self = Self {
51+
top_left: 0.0,
52+
top_right: 0.0,
53+
bottom_right: 0.0,
54+
bottom_left: 0.0,
55+
};
56+
57+
fn max(self) -> f64 {
58+
self.top_left
59+
.max(self.top_right)
60+
.max(self.bottom_right)
61+
.max(self.bottom_left)
62+
}
3863
}
3964

4065
#[derive(Clone, Default)]
@@ -497,15 +522,24 @@ impl AndroidBridge {
497522

498523
pub fn chrome_profile(&self, id: &str) -> Result<Value, AppError> {
499524
let serial = self.serial_for_id(id)?;
500-
let (width, height) = self.screen_size_for_serial(&serial)?;
525+
let metrics = self.display_metrics_for_serial(&serial)?;
526+
let width = metrics.width;
527+
let height = metrics.height;
528+
let radii = metrics.corner_radii;
501529
Ok(json!({
502530
"totalWidth": width,
503531
"totalHeight": height,
504532
"screenX": 0,
505533
"screenY": 0,
506534
"screenWidth": width,
507535
"screenHeight": height,
508-
"cornerRadius": 0,
536+
"cornerRadius": radii.max(),
537+
"cornerRadii": {
538+
"topLeft": radii.top_left,
539+
"topRight": radii.top_right,
540+
"bottomRight": radii.bottom_right,
541+
"bottomLeft": radii.bottom_left,
542+
},
509543
"hasScreenMask": false,
510544
}))
511545
}
@@ -744,6 +778,7 @@ impl AndroidBridge {
744778
width: 0.0,
745779
height: 0.0,
746780
rotation_quarter_turns: 0,
781+
corner_radii: AndroidCornerRadii::ZERO,
747782
});
748783
json!({
749784
"displayReady": metrics.width > 0.0 && metrics.height > 0.0,
@@ -903,6 +938,7 @@ impl AndroidBridge {
903938
width,
904939
height,
905940
rotation_quarter_turns: 0,
941+
corner_radii: AndroidCornerRadii::ZERO,
906942
})
907943
}
908944

@@ -1370,6 +1406,7 @@ fn android_display_metrics_cache() -> &'static Mutex<DisplayMetricsCache> {
13701406

13711407
fn parse_android_display_metrics(output: &str) -> Option<AndroidDisplayMetrics> {
13721408
let rotation = parse_android_display_rotation(output).unwrap_or(0);
1409+
let corner_radii = parse_android_rounded_corners(output).unwrap_or(AndroidCornerRadii::ZERO);
13731410
if let Some(line) = output
13741411
.lines()
13751412
.find(|line| line.contains("mOverrideDisplayInfo=DisplayInfo"))
@@ -1379,6 +1416,7 @@ fn parse_android_display_metrics(output: &str) -> Option<AndroidDisplayMetrics>
13791416
width,
13801417
height,
13811418
rotation_quarter_turns: rotation,
1419+
corner_radii,
13821420
});
13831421
}
13841422
}
@@ -1387,11 +1425,44 @@ fn parse_android_display_metrics(output: &str) -> Option<AndroidDisplayMetrics>
13871425
width,
13881426
height,
13891427
rotation_quarter_turns: rotation,
1428+
corner_radii,
13901429
});
13911430
}
13921431
None
13931432
}
13941433

1434+
fn parse_android_rounded_corners(output: &str) -> Option<AndroidCornerRadii> {
1435+
let mut radii = AndroidCornerRadii::ZERO;
1436+
let mut found = false;
1437+
1438+
for chunk in output.split("RoundedCorner{").skip(1) {
1439+
let section = chunk.split('}').next().unwrap_or(chunk);
1440+
let Some(position) = parse_named_value(section, "position=") else {
1441+
continue;
1442+
};
1443+
let Some(radius) =
1444+
parse_named_value(section, "radius=").and_then(|value| value.parse::<f64>().ok())
1445+
else {
1446+
continue;
1447+
};
1448+
match position {
1449+
"TopLeft" => radii.top_left = radius,
1450+
"TopRight" => radii.top_right = radius,
1451+
"BottomRight" => radii.bottom_right = radius,
1452+
"BottomLeft" => radii.bottom_left = radius,
1453+
_ => continue,
1454+
}
1455+
found = true;
1456+
}
1457+
1458+
found.then_some(radii)
1459+
}
1460+
1461+
fn parse_named_value<'a>(input: &'a str, key: &str) -> Option<&'a str> {
1462+
let (_, value) = input.split_once(key)?;
1463+
Some(value.split([',', '}', ']']).next().unwrap_or(value).trim())
1464+
}
1465+
13951466
fn parse_android_display_rotation(output: &str) -> Option<u16> {
13961467
output
13971468
.lines()
@@ -1734,6 +1805,7 @@ mod tests {
17341805
width: 2400.0,
17351806
height: 1080.0,
17361807
rotation_quarter_turns: 3,
1808+
corner_radii: AndroidCornerRadii::ZERO,
17371809
})
17381810
);
17391811
}
@@ -1751,6 +1823,31 @@ mod tests {
17511823
width: 1080.0,
17521824
height: 2400.0,
17531825
rotation_quarter_turns: 0,
1826+
corner_radii: AndroidCornerRadii::ZERO,
1827+
})
1828+
);
1829+
}
1830+
1831+
#[test]
1832+
fn parse_android_display_metrics_reads_rounded_corners() {
1833+
let output = r#"
1834+
DisplayDeviceInfo{"Built-in Screen", 1080 x 2400, roundedCorners RoundedCorners{[RoundedCorner{position=TopLeft, radius=104, center=Point(104, 104)}, RoundedCorner{position=TopRight, radius=104, center=Point(976, 104)}, RoundedCorner{position=BottomRight, radius=102, center=Point(978, 2298)}, RoundedCorner{position=BottomLeft, radius=102, center=Point(102, 2298)}]}}
1835+
mViewports=[DisplayViewport{type=INTERNAL, logicalFrame=Rect(0, 0 - 1080, 2400), physicalFrame=Rect(0, 0 - 1080, 2400)}]
1836+
mCurrentOrientation=0
1837+
"#;
1838+
1839+
assert_eq!(
1840+
parse_android_display_metrics(output),
1841+
Some(AndroidDisplayMetrics {
1842+
width: 1080.0,
1843+
height: 2400.0,
1844+
rotation_quarter_turns: 0,
1845+
corner_radii: AndroidCornerRadii {
1846+
top_left: 104.0,
1847+
top_right: 104.0,
1848+
bottom_right: 102.0,
1849+
bottom_left: 102.0,
1850+
},
17541851
})
17551852
);
17561853
}

0 commit comments

Comments
 (0)