diff --git a/ui/shared/power/screen.js b/ui/shared/power/screen.js index 9c75b52..1ef1a82 100644 --- a/ui/shared/power/screen.js +++ b/ui/shared/power/screen.js @@ -1,6 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Screen power (on/off), on top of `navigator.embedder.power`. +// +// Darkening the panel also raises the input shield, and lighting it lowers it. + +import { + lowerShield, + raiseShield, +} from "beaver://shared/power/screen_shield.js"; // The DOM power object, or null on a page/build that can't reach it. function powerApi() { @@ -44,10 +51,13 @@ class ScreenPower { } off() { + // Shield first, before the panel goes dark. + raiseShield({ onWake: () => this.wake() }); this.set(false); } wake() { + lowerShield(); this.set(true); } } diff --git a/ui/shared/power/screen_shield.js b/ui/shared/power/screen_shield.js new file mode 100644 index 0000000..0bcde61 --- /dev/null +++ b/ui/shared/power/screen_shield.js @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// The shield: what covers the screen while the panel is dark. +// +// This manages the wake gesture: a double tap wake. + +// Above the dusk veil (9000), which is itself above --z-modal: the fade may +// still be running when the panel is cut. +const SHIELD_Z_INDEX = 9500; + +// Two taps inside this window, and this far apart at most, count as a double +// tap. +const DOUBLE_TAP_MS = 500; +const DOUBLE_TAP_SLOP_PX = 80; + +let shield = null; +let lastTap = null; +let wakeAction = null; + +/** + * Cover the screen. Idempotent; the first caller's `onWake` stands. + * + * @param {object} [options] + * @param {Function} [options.onWake] Run when the user double taps. + */ +export function raiseShield({ onWake } = {}) { + if (shield) { + return; + } + lastTap = null; + wakeAction = onWake ?? null; + + shield = document.createElement("div"); + shield.className = "screen-shield"; + shield.style.cssText = [ + "position: fixed", + "inset: 0", + `z-index: ${SHIELD_Z_INDEX}`, + // Transparent: the dusk veil and the screen-off effect do the visuals. + // This element exists only to catch input. + "background: transparent", + // Servo ignores touch-action, so the handlers below are what actually stop + // the gesture. Declared anyway for correctness elsewhere. + "touch-action: none", + ].join(";"); + + // Capture phase, and every pointer/touch stage: a swallowed `down` whose + // `move` and `up` still landed would leave the page mid-gesture. + for (const type of [ + "pointerdown", + "pointermove", + "pointerup", + "pointercancel", + "touchstart", + "touchmove", + "touchend", + "touchcancel", + "click", + "dblclick", + "contextmenu", + "wheel", + ]) { + shield.addEventListener(type, swallow, true); + } + shield.addEventListener("pointerup", onTap, true); + + document.body.appendChild(shield); +} + +// Uncover the screen. Idempotent. +export function lowerShield() { + shield?.remove(); + shield = null; + lastTap = null; + wakeAction = null; +} + +// True while the shield is up, i.e. while the screen is meant to be dark. +export function shieldIsUp() { + return shield !== null; +} + +function swallow(event) { + event.preventDefault(); + event.stopPropagation(); +} + +// A double tap wakes. Tracked on pointerup rather than pointerdown so a press +// that turns into a drag does not count as a tap. +function onTap(event) { + const now = event.timeStamp; + const previous = lastTap; + lastTap = { x: event.clientX, y: event.clientY, at: now }; + + if ( + previous && + now - previous.at <= DOUBLE_TAP_MS && + Math.abs(event.clientX - previous.x) <= DOUBLE_TAP_SLOP_PX && + Math.abs(event.clientY - previous.y) <= DOUBLE_TAP_SLOP_PX + ) { + const wake = wakeAction; + lowerShield(); + wake?.(); + } +}