Administra varias pantallas con la API de Window Management

Obtén información sobre las pantallas conectadas y coloca ventanas en relación con ellas.

API de Window Management

La API de Window Management te permite enumerar las pantallas conectadas a tu máquina y colocar ventanas en pantallas específicas.

Casos de uso sugeridos

Estos son algunos ejemplos de sitios que pueden usar esta API:

  • Los editores de gráficos multiventana, como Gimp, pueden colocar varias herramientas de edición en ventanas posicionadas con precisión.
  • Las mesas de negociación virtuales pueden mostrar las tendencias del mercado en varias ventanas, cualquiera de las cuales se puede ver en modo de pantalla completa.
  • Las apps de presentación de diapositivas pueden mostrar las notas del orador en la pantalla principal interna y la presentación en un proyector externo.

Cómo usar la API de Window Management

El problema

Lamentablemente, el enfoque probado para controlar ventanas, Window.open(), no tiene en cuenta las pantallas adicionales. Si bien algunos aspectos de esta API parecen un poco arcaicos, como su parámetro windowFeatures DOMString, nos ha sido de gran utilidad a lo largo de los años. Para especificar la posición de una ventana, puedes pasar las coordenadas como left y top (o screenX y screenY, respectivamente) y pasar el tamaño deseado como width y height (o innerWidth y innerHeight, respectivamente). Por ejemplo, para abrir una ventana de 400 × 300 a 50 píxeles de la izquierda y 50 píxeles de la parte superior, puedes usar el siguiente código:

const popup = window.open(
  'https://example.com/',
  'My Popup',
  'left=50,top=50,width=400,height=300',
);

Puedes obtener información sobre la pantalla actual consultando la propiedad window.screen, que devuelve un objeto Screen. Este es el resultado en mi MacBook Pro de 13":

window.screen;
/* Output from my MacBook Pro 13″:
  availHeight: 969
  availLeft: 0
  availTop: 25
  availWidth: 1680
  colorDepth: 30
  height: 1050
  isExtended: true
  onchange: null
  orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
  pixelDepth: 30
  width: 1680
*/

Al igual que la mayoría de las personas que trabajan en tecnología, tuve que adaptarme a la nueva realidad laboral y configurar mi oficina personal en casa. El mío se ve como el de la foto a continuación (si te interesa, puedes leer los detalles completos sobre mi configuración). El iPad que está junto a mi MacBook está conectado a la laptop a través de Sidecar, por lo que, cuando lo necesito, puedo convertir rápidamente el iPad en una segunda pantalla.

Banco escolar sobre dos sillas. Sobre el banco escolar, hay cajas de zapatos que sostienen una laptop y dos iPads a su alrededor.
Una configuración de varias pantallas.

Si quiero aprovechar la pantalla más grande, puedo colocar la ventana emergente del ejemplo de código anterior en la segunda pantalla. Yo lo hago de la siguiente manera:

popup.moveTo(2500, 50);

Esta es una estimación aproximada, ya que no hay forma de conocer las dimensiones de la segunda pantalla. La información de window.screen solo abarca la pantalla integrada, pero no la del iPad. El width informado de la pantalla integrada era de 1680 píxeles, por lo que cambiar a 2500 píxeles podría funcionar para desplazar la ventana al iPad, ya que que se encuentra a la derecha de mi MacBook. ¿Cómo puedo hacer esto en el caso general? Resulta que hay una mejor manera que adivinar. Esa forma es la API de Window Management.

Detección de características

Para verificar si se admite la API de Window Management, usa lo siguiente:

if ('getScreenDetails' in window) {
  // The Window Management API is supported.
}

El permiso window-management

Antes de poder usar la API de Window Management, debo pedirle permiso al usuario para hacerlo. El permiso window-management se puede consultar con la API de Permissions de la siguiente manera:

let granted = false;
try {
  const { state } = await navigator.permissions.query({ name: 'window-management' });
  granted = state === 'granted';
} catch {
  // Nothing.
}

Mientras se usan navegadores con el nombre de permiso anterior y el nuevo, asegúrate de usar código defensivo cuando solicites permiso, como en el siguiente ejemplo.

async function getWindowManagementPermissionState() {
  let state;
  // The new permission name.
  try {
    ({ state } = await navigator.permissions.query({
      name: "window-management",
    }));
  } catch (err) {
    return `${err.name}: ${err.message}`;
  }
  return state;
}

document.querySelector("button").addEventListener("click", async () => {
  const state = await getWindowManagementPermissionState();
  document.querySelector("pre").textContent = state;
});

El navegador puede optar por mostrar el mensaje de permiso de forma dinámica en el primer intento de usar cualquiera de los métodos de la nueva API. Sigue leyendo para obtener más información.

La propiedad window.screen.isExtended

Para saber si hay más de una pantalla conectada a mi dispositivo, accedo a la propiedad window.screen.isExtended. Devuelve true o false. En mi configuración, devuelve true.

window.screen.isExtended;
// Returns `true` or `false`.

El método getScreenDetails()

Ahora que sé que la configuración actual es de varias pantallas, puedo obtener más información sobre la segunda pantalla con Window.getScreenDetails(). Si llamo a esta función, se mostrará un mensaje de permiso que me preguntará si el sitio puede abrir y colocar ventanas en mi pantalla. La función devuelve una promesa que se resuelve con un objeto ScreenDetailed. En mi MacBook Pro 13 con un iPad conectado, esto incluye un campo screens con dos objetos ScreenDetailed:

await window.getScreenDetails();
/* Output from my MacBook Pro 13″ with the iPad attached:
{
  currentScreen: ScreenDetailed {left: 0, top: 0, isPrimary: true, isInternal: true, devicePixelRatio: 2, …}
  oncurrentscreenchange: null
  onscreenschange: null
  screens: [{
    // The MacBook Pro
    availHeight: 969
    availLeft: 0
    availTop: 25
    availWidth: 1680
    colorDepth: 30
    devicePixelRatio: 2
    height: 1050
    isExtended: true
    isInternal: true
    isPrimary: true
    label: "Built-in Retina Display"
    left: 0
    onchange: null
    orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
    pixelDepth: 30
    top: 0
    width: 1680
  },
  {
    // The iPad
    availHeight: 999
    availLeft: 1680
    availTop: 25
    availWidth: 1366
    colorDepth: 24
    devicePixelRatio: 2
    height: 1024
    isExtended: true
    isInternal: false
    isPrimary: false
    label: "Sidecar Display (AirPlay)"
    left: 1680
    onchange: null
    orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
    pixelDepth: 24
    top: 0
    width: 1366
  }]
}
*/

La información sobre las pantallas conectadas está disponible en el array screens. Observa cómo el valor de left para el iPad comienza en 1680, que es exactamente el width de la pantalla integrada. Esto me permite determinar exactamente cómo se organizan las pantallas de forma lógica (una al lado de la otra, una encima de la otra, etcétera). Ahora también hay datos para cada pantalla que muestran si es una isInternal y si es una isPrimary. Ten en cuenta que la pantalla integrada no es necesariamente la pantalla principal.

El campo currentScreen es un objeto activo que corresponde al window.screen actual. El objeto se actualiza en las posiciones de ventanas en varias pantallas o en los cambios de dispositivo.

El evento screenschange

Lo único que falta ahora es una forma de detectar cuándo cambia la configuración de mi pantalla. Un nuevo evento, screenschange, hace exactamente eso: se activa cada vez que se modifica la constelación de la pantalla. (Ten en cuenta que "screens" está en plural en el nombre del evento). Esto significa que el evento se activa cada vez que se conecta o desconecta una pantalla nueva o una pantalla existente (física o virtualmente en el caso de Sidecar).

Ten en cuenta que debes buscar los detalles de la nueva pantalla de forma asíncrona, ya que el evento screenschange en sí no proporciona estos datos. Para buscar los detalles de la pantalla, usa el objeto activo de una interfaz Screens almacenada en caché.

const screenDetails = await window.getScreenDetails();
let cachedScreensLength = screenDetails.screens.length;
screenDetails.addEventListener('screenschange', (event) => {
  if (screenDetails.screens.length !== cachedScreensLength) {
    console.log(
      `The screen count changed from ${cachedScreensLength} to ${screenDetails.screens.length}`,
    );
    cachedScreensLength = screenDetails.screens.length;
  }
});

El evento currentscreenchange

Si solo me interesan los cambios en la pantalla actual (es decir, el valor del objeto activo currentScreen), puedo escuchar el evento currentscreenchange.

const screenDetails = await window.getScreenDetails();
screenDetails.addEventListener('currentscreenchange', async (event) => {
  const details = screenDetails.currentScreen;
  console.log('The current screen has changed.', event, details);
});

El evento change

Por último, si solo me interesan los cambios en una pantalla concreta, puedo escuchar el evento change de esa pantalla.

const firstScreen = (await window.getScreenDetails())[0];
firstScreen.addEventListener('change', async (event) => {
  console.log('The first screen has changed.', event, firstScreen);
});

Nuevas opciones de pantalla completa

Hasta ahora, podías solicitar que los elementos se mostraran en modo de pantalla completa a través del método requestFullScreen(), cuyo nombre es muy apropiado. El método toma un parámetro options en el que puedes pasar FullscreenOptions. Hasta el momento, su única propiedad fue navigationUI. La API de Window Management agrega una nueva propiedad screen que te permite determinar en qué pantalla iniciar la vista de pantalla completa. Por ejemplo, si quieres que la pantalla principal se muestre en pantalla completa, haz lo siguiente:

try {
  const primaryScreen = (await getScreenDetails()).screens.filter((screen) => screen.isPrimary)[0];
  await document.body.requestFullscreen({ screen: primaryScreen });
} catch (err) {
  console.error(err.name, err.message);
}

Polyfill

No es posible agregar un polyfill a la API de Window Management, pero puedes agregar un shim a su forma para que puedas programar exclusivamente en función de la nueva API:

if (!('getScreenDetails' in window)) {
  // Returning a one-element array with the current screen,
  // noting that there might be more.
  window.getScreenDetails = async () => [window.screen];
  // Set to `false`, noting that this might be a lie.
  window.screen.isExtended = false;
}

Los otros aspectos de la API, es decir, los diversos eventos de cambio de pantalla y la propiedad screen de FullscreenOptions, simplemente nunca se activarían o serían ignorados de forma silenciosa por los navegadores no compatibles.

Demostración

Si eres como yo, sigues de cerca el desarrollo de las distintas criptomonedas. (En realidad, no lo hago porque amo este planeta, pero, para los fines de este artículo, supón que sí). Para hacer un seguimiento de las criptomonedas que poseo, desarrollé una app web que me permite observar los mercados en todas las situaciones de la vida, como desde la comodidad de mi cama, donde tengo una configuración decente de una sola pantalla.

Enorme pantalla de TV al final de una cama con las piernas del autor parcialmente visibles. En la pantalla, se ve una mesa de operaciones de criptomonedas falsa.
Relajarse y observar los mercados.

Como se trata de criptomonedas, los mercados pueden volverse frenéticos en cualquier momento. Si esto sucede, puedo cambiarme rápidamente a mi escritorio, donde tengo una configuración de varias pantallas. Puedo hacer clic en la ventana de cualquier moneda y ver rápidamente todos los detalles en una vista de pantalla completa en la pantalla opuesta. A continuación, se incluye una foto reciente mía que se tomó durante el último baño de sangre de YCY. Me tomó por sorpresa y me dejó con las manos en la cara.

El autor con las manos en su rostro de pánico mirando el escritorio de comercio de criptomonedas falsas.
Entra en pánico al ver la masacre de la comparación interanual.

Puedes probar la demostración que se incluye a continuación o ver su código fuente en GitHub.

Seguridad y permisos

El equipo de Chrome diseñó y, luego, implementó la API de Window Management con los principios fundamentales definidos en Controlling Access to Powerful Web Platform Features, incluidos el control del usuario, la transparencia y la ergonomía. La API de Window Management expone información nueva sobre las pantallas conectadas a un dispositivo, lo que aumenta la superficie de huellas digitales de los usuarios, en especial, de aquellos que tienen varias pantallas conectadas de forma constante a sus dispositivos. Como una medida de mitigación de este problema de privacidad, las propiedades de pantalla expuestas se limitan al mínimo necesario para los casos de uso de colocación comunes. Los sitios necesitan el permiso del usuario para obtener información de multipantalla y colocar ventanas en otras pantallas. Si bien Chromium devuelve etiquetas de pantalla detalladas, los navegadores pueden devolver etiquetas menos descriptivas (o incluso vacías).

Control de usuarios

El usuario tiene control total sobre la exposición de su configuración. Pueden aceptar o rechazar el mensaje de permiso, y revocar un permiso otorgado anteriormente a través de la función de información del sitio en el navegador.

Control empresarial

Los usuarios de Chrome Enterprise pueden controlar varios aspectos de la API de Window Management, como se describe en la sección correspondiente de la configuración de los grupos de políticas atómicas.

Transparencia

El hecho de que se haya otorgado el permiso para usar la API de Window Management se expone en la información del sitio del navegador y también se puede consultar a través de la API de Permissions.

Persistencia de permisos

El navegador conserva los permisos otorgados. El permiso se puede revocar a través de la información del sitio del navegador.

Comentarios

El equipo de Chrome quiere conocer tu experiencia con la API de Window Management.

Cuéntanos sobre el diseño de la API

¿Hay algo sobre la API que no funciona como esperabas? ¿O faltan métodos o propiedades que necesitas para implementar tu idea? ¿Tienes alguna pregunta o comentario sobre el modelo de seguridad?

  • Informa un problema de especificación en el repositorio de GitHub correspondiente o agrega tus ideas a un problema existente.

Informa un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿O la implementación es diferente de la especificación?

  • Presenta un error en new.crbug.com. Asegúrate de incluir tantos detalles como puedas, instrucciones simples para reproducir el error y, luego, ingresa Blink>Screen>MultiScreen en el cuadro Components.

Cómo mostrar compatibilidad con la API

¿Planeas usar la API de Window Management? Tu apoyo público ayuda al equipo de Chrome a priorizar funciones y muestra a otros proveedores de navegadores lo importante que es admitirlas.

Vínculos útiles

Agradecimientos

Victor Costan, Joshua Bell y Mike Wasserman editaron las especificaciones de la API de Window Management. La API fue implementada por Mike Wasserman y Adrienne Walker. Joe Medley, François Beaufort y Kayce Basques revisaron este artículo. Gracias a Laura Torrent Puig por las fotos.