Skip to content

Commit c331705

Browse files
committed
feat: soften iPhone/iPad chrome button press depth
1 parent 8b52248 commit c331705

6 files changed

Lines changed: 143 additions & 17 deletions

File tree

packages/client/src/app/AppShell.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
shouldRenderNativeChrome,
5252
simulatorHasFixedOrientation,
5353
simulatorRuntimeLabel,
54+
simulatorUsesInsetChromeButtons,
5455
} from "../features/simulators/simulatorDisplay";
5556
import { useSimulatorList } from "../features/simulators/useSimulatorList";
5657
import { sendWebRtcControlMessage } from "../features/stream/streamWorkerClient";
@@ -147,7 +148,7 @@ const STREAM_TRANSPORT_VALUES = new Set<StreamTransport>([
147148
"webrtc",
148149
]);
149150
const MOBILE_VIEWPORT_MEDIA_QUERY = "(max-width: 600px)";
150-
const CHROME_RENDERER_ASSET_VERSION = "chrome-renderer-watch-bezel-inset-22";
151+
const CHROME_RENDERER_ASSET_VERSION = "chrome-renderer-button-overlay-23";
151152
clearLegacyVolatileUiState();
152153

153154
interface StreamQualityResponse {
@@ -1015,6 +1016,9 @@ export function AppShell({
10151016
const chromeHasInteractiveButtons = Boolean(
10161017
viewportChromeProfile?.buttons?.length,
10171018
);
1019+
const chromeUsesButtonOverlay =
1020+
chromeHasInteractiveButtons &&
1021+
simulatorUsesInsetChromeButtons(selectedSimulator);
10181022
const chromeHasCrown = Boolean(
10191023
viewportChromeProfile?.buttons?.some(
10201024
(button) =>
@@ -1033,14 +1037,23 @@ export function AppShell({
10331037
selectedSimulator?.udid,
10341038
chromeGeometryStamp,
10351039
CHROME_RENDERER_ASSET_VERSION,
1036-
chromeHasInteractiveButtons ? "baked-buttons" : "no-buttons",
1040+
chromeUsesButtonOverlay
1041+
? "overlay-buttons"
1042+
: chromeHasInteractiveButtons
1043+
? "baked-buttons"
1044+
: "no-buttons",
10371045
chromeHasCrown ? "crown" : "no-crown",
10381046
]
10391047
.filter(Boolean)
10401048
.join(":");
1041-
const chromeButtonsRenderedInChrome = chromeHasInteractiveButtons;
1049+
const chromeButtonsRenderedInChrome =
1050+
chromeHasInteractiveButtons && !chromeUsesButtonOverlay;
10421051
const chromeUrl = selectedSimulator
1043-
? buildChromeUrl(selectedSimulator.udid, chromeAssetStamp, true)
1052+
? buildChromeUrl(
1053+
selectedSimulator.udid,
1054+
chromeAssetStamp,
1055+
chromeButtonsRenderedInChrome,
1056+
)
10441057
: "";
10451058
const chromeButtonUrl = useCallback(
10461059
(button: string, pressed = false) =>

packages/client/src/features/simulators/simulatorDisplay.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
shouldRenderNativeChrome,
66
simulatorHasFixedOrientation,
77
simulatorRuntimeLabel,
8+
simulatorUsesInsetChromeButtons,
89
} from "./simulatorDisplay";
910

1011
function simulator(
@@ -94,4 +95,31 @@ describe("simulatorDisplay", () => {
9495
),
9596
).toBe(false);
9697
});
98+
99+
it("uses inset overlay chrome buttons only for iPhone and iPad simulators", () => {
100+
expect(
101+
simulatorUsesInsetChromeButtons(
102+
simulator({
103+
deviceTypeIdentifier:
104+
"com.apple.CoreSimulator.SimDeviceType.iPhone-17",
105+
}),
106+
),
107+
).toBe(true);
108+
expect(
109+
simulatorUsesInsetChromeButtons(
110+
simulator({
111+
deviceTypeIdentifier:
112+
"com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M4",
113+
}),
114+
),
115+
).toBe(true);
116+
expect(
117+
simulatorUsesInsetChromeButtons(
118+
simulator({
119+
deviceTypeIdentifier:
120+
"com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Ultra-3-49mm",
121+
}),
122+
),
123+
).toBe(false);
124+
});
97125
});

packages/client/src/features/simulators/simulatorDisplay.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ export function simulatorHasFixedOrientation(
4747
);
4848
}
4949

50+
export function simulatorUsesInsetChromeButtons(
51+
simulator: SimulatorMetadata | null,
52+
): boolean {
53+
if (!simulator || simulator.platform === "android-emulator") {
54+
return false;
55+
}
56+
const metadata = simulatorMetadataText(simulator);
57+
return metadata.includes("iphone") || metadata.includes("ipad");
58+
}
59+
5060
function simulatorMetadataText(simulator: SimulatorMetadata): string {
5161
return [
5262
simulator.name,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import type { ChromeButtonProfile } from "../../api/types";
4+
import { chromeButtonMotionVariables } from "./DeviceChrome";
5+
6+
describe("chromeButtonMotionVariables", () => {
7+
it("rests halfway inward, hovers at the original button position, and presses farther inward", () => {
8+
const button: ChromeButtonProfile = {
9+
name: "side-button",
10+
x: 100,
11+
y: 20,
12+
width: 10,
13+
height: 20,
14+
normalOffset: { x: 0, y: 0 },
15+
rolloverOffset: { x: 4, y: -2 },
16+
};
17+
18+
expect(chromeButtonMotionVariables(button)).toEqual({
19+
"--button-rest-x": "-20%",
20+
"--button-rest-y": "5%",
21+
"--button-hover-x": "0%",
22+
"--button-hover-y": "0%",
23+
"--button-pressed-x": "-34%",
24+
"--button-pressed-y": "8.5%",
25+
});
26+
});
27+
28+
it("keeps buttons without a rollover offset stationary", () => {
29+
const button: ChromeButtonProfile = {
30+
name: "home",
31+
x: 0,
32+
y: 0,
33+
width: 44,
34+
height: 44,
35+
};
36+
37+
expect(chromeButtonMotionVariables(button)).toEqual({
38+
"--button-rest-x": "0%",
39+
"--button-rest-y": "0%",
40+
"--button-hover-x": "0%",
41+
"--button-hover-y": "0%",
42+
"--button-pressed-x": "0%",
43+
"--button-pressed-y": "0%",
44+
});
45+
});
46+
});

packages/client/src/features/viewport/DeviceChrome.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,44 @@ const CHROME_BUTTON_WIRE_NAMES: Record<string, string> = {
225225
"volume-down": "volume-down",
226226
"volume-up": "volume-up",
227227
};
228+
const CHROME_BUTTON_REST_INSET_RATIO = 0.5;
229+
const CHROME_BUTTON_PRESSED_INSET_RATIO = 0.85;
230+
231+
export function chromeButtonMotionVariables(button: ChromeButtonProfile) {
232+
const normalOffset = button.normalOffset ?? { x: 0, y: 0 };
233+
const rolloverOffset = button.rolloverOffset ?? normalOffset;
234+
const inwardDelta = {
235+
x: normalOffset.x - rolloverOffset.x,
236+
y: normalOffset.y - rolloverOffset.y,
237+
};
238+
const restOffset = {
239+
x: inwardDelta.x * CHROME_BUTTON_REST_INSET_RATIO,
240+
y: inwardDelta.y * CHROME_BUTTON_REST_INSET_RATIO,
241+
};
242+
const pressedOffset = {
243+
x: inwardDelta.x * CHROME_BUTTON_PRESSED_INSET_RATIO,
244+
y: inwardDelta.y * CHROME_BUTTON_PRESSED_INSET_RATIO,
245+
};
246+
const width = Math.max(button.width, 1);
247+
const height = Math.max(button.height, 1);
248+
249+
return {
250+
"--button-rest-x": `${(restOffset.x / width) * 100}%`,
251+
"--button-rest-y": `${(restOffset.y / height) * 100}%`,
252+
"--button-hover-x": "0%",
253+
"--button-hover-y": "0%",
254+
"--button-pressed-x": `${(pressedOffset.x / width) * 100}%`,
255+
"--button-pressed-y": `${(pressedOffset.y / height) * 100}%`,
256+
} as Record<
257+
| "--button-rest-x"
258+
| "--button-rest-y"
259+
| "--button-hover-x"
260+
| "--button-hover-y"
261+
| "--button-pressed-x"
262+
| "--button-pressed-y",
263+
string
264+
>;
265+
}
228266

229267
function ChromeButtonOverlay({
230268
chromeButtonUrl,
@@ -305,12 +343,6 @@ function ChromeButtonHitTarget({
305343
const pressedRef = useRef(false);
306344
const [pressed, setPressed] = useState(false);
307345
const label = button.label || humanizeChromeButtonName(button.name);
308-
const rolloverDelta = button.rolloverOffset
309-
? {
310-
x: button.rolloverOffset.x - (button.normalOffset?.x ?? 0),
311-
y: button.rolloverOffset.y - (button.normalOffset?.y ?? 0),
312-
}
313-
: { x: 0, y: 0 };
314346
const imageUrl = chromeButtonUrl(button.name, false);
315347
const pressedImageUrl = button.imageDownName
316348
? chromeButtonUrl(button.name, true)
@@ -324,12 +356,7 @@ function ChromeButtonHitTarget({
324356
left: `${(button.x / totalWidth) * 100}%`,
325357
top: `${(button.y / totalHeight) * 100}%`,
326358
width: `${(button.width / totalWidth) * 100}%`,
327-
"--button-rest-x": "0%",
328-
"--button-rest-y": "0%",
329-
"--button-hover-x": `${(rolloverDelta.x / Math.max(button.width, 1)) * 100}%`,
330-
"--button-hover-y": `${(rolloverDelta.y / Math.max(button.height, 1)) * 100}%`,
331-
"--button-pressed-x": `${(-rolloverDelta.x / Math.max(button.width, 1)) * 100}%`,
332-
"--button-pressed-y": `${(-rolloverDelta.y / Math.max(button.height, 1)) * 100}%`,
359+
...chromeButtonMotionVariables(button),
333360
} as CSSProperties &
334361
Record<
335362
| "--button-rest-x"

packages/client/src/styles/components.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2414,8 +2414,9 @@ a.hierarchy-node-source:hover,
24142414
pointer-events: auto;
24152415
touch-action: none;
24162416
transform: translate3d(var(--button-rest-x, 0), var(--button-rest-y, 0), 0);
2417-
transition: transform 130ms cubic-bezier(0.2, 0.8, 0.2, 1);
2417+
transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
24182418
-webkit-tap-highlight-color: transparent;
2419+
will-change: transform;
24192420
z-index: 1;
24202421
}
24212422

@@ -2453,6 +2454,7 @@ a.hierarchy-node-source:hover,
24532454
var(--button-pressed-y, var(--button-rest-y, 0)),
24542455
0
24552456
);
2457+
transition-duration: 90ms;
24562458
}
24572459

24582460
.pan-enabled .device-bezel,

0 commit comments

Comments
 (0)