Administra varias pantallas con la API de Window Management

Obtén información sobre las pantallas conectadas y las ventanas de posición en relación con esas pantallas.

API de Window Management

La API de Window Management te permite enumerar las pantallas conectadas a tu máquina y ubicar 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 junto con Gimp pueden colocar varias herramientas de edición en ventanas bien posicionadas.
  • Las mesas de operaciones virtuales pueden mostrar tendencias de mercados en varias ventanas, cualquiera de las cuales se puede ver en modo de pantalla completa.
  • Las apps de presentación de diapositivas pueden mostrar 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 en el tiempo para controlar ventanas, Window.open(), desconoce las pantallas adicionales. Si bien algunos aspectos de esta API parecen un poco arcaicos, como su parámetro DOMString de windowFeatures, nos ha servido mucho 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 desde la izquierda y a 50 píxeles desde la parte superior, este es el código que puedes usar:

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 si consultas la propiedad window.screen, que muestra 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
*/

Como 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 en la siguiente foto (si te interesa, puedes leer los detalles completos sobre mi configuración). El iPad junto a mi MacBook está conectado a la laptop mediante Sidecar, por lo que, cada vez que lo necesite, puedo convertir rápidamente el iPad en una segunda pantalla.

Banco de la escuela sobre dos sillas. Encima del banco de la escuela, hay cajas de zapatos que sostienen una laptop y dos iPads a su alrededor.
Una configuración multipantalla

Si quiero aprovechar la pantalla más grande, puedo colocar la ventana emergente de la muestra de código anterior en la segunda pantalla. Lo hago así:

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 mover a 2500 píxeles podría cambiar la ventana al iPad, ya que conozco que se encuentra a la derecha de mi MacBook. ¿Cómo puedo hacer esto en un caso general? Resulta que hay una mejor manera que adivinar. De esa manera, se crea la API de Window Management.

Detección de funciones

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 al usuario permiso para hacerlo. Se puede consultar el permiso window-management 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 los navegadores con los nombres de los permisos anteriores y nuevos, asegúrate de usar código defensivo cuando solicites permiso, como se muestra 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 elegir mostrar la solicitud de permiso de forma dinámica en el primer intento de usar cualquiera de los métodos de la API nueva. Continúa 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. Muestra true o false. Para mi configuración, muestra true.

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

El método getScreenDetails()

Ahora que sé que la configuración actual es multipantalla, puedo obtener más información sobre la segunda pantalla con Window.getScreenDetails(). Si llamas 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 muestra 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 están organizadas lógicamente las pantallas (una al lado de la otra, una encima de la otra, etc.). Ahora también hay datos para cada pantalla que muestran si es de tipo isInternal y de isPrimary. Ten en cuenta que la pantalla integrada no es necesariamente la pantalla principal.

El campo currentScreen es un objeto activo correspondiente a la window.screen actual. El objeto se actualiza cuando cambia el dispositivo o las posiciones de ventanas en varias pantallas.

El evento screenschange

Lo único que falta ahora es una forma de detectar cuándo cambia la configuración de mi pantalla. Un evento nuevo, screenschange, hace exactamente eso: se activa cada vez que se modifica la constelación de pantallas. (ten en cuenta que "pantallas" 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 existente (de forma física o virtual en el caso de Sidecar).

Ten en cuenta que debes buscar los nuevos detalles de la pantalla de forma asíncrona, ya que el evento screenschange no proporcionará 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 bien denominado requestFullScreen(). El método toma un parámetro options en el que puedes pasar FullscreenOptions. Hasta ahora, su única propiedad ha sido 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 sea completa:

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 aplicar polyfills de la API de Window Management, pero puedes corregir su forma para codificar de forma exclusiva con 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, nunca se activan ni se ignoran en silencio, respectivamente, de los navegadores no compatibles.

Demostración

Si son como yo, están atentos al desarrollo de las diversas criptomonedas. En realidad, no me gusta mucho este planeta porque amo este planeta, pero a los fines de este artículo, supongo que sí. Para hacer un seguimiento de las criptomonedas que poseo, desarrollé una app web que me permite mirar los mercados en todas las situaciones de la vida, como desde la comodidad de mi cama, donde tengo una configuración adecuada de una sola pantalla.

Pantalla de TV gigante al final de una cama con las piernas del autor parcialmente visibles. En la pantalla, hay una mesa de operaciones de criptomonedas falsa.
Relajarse y mirar los mercados

Como hablamos de las criptomonedas, los mercados pueden volverse frenéticos en cualquier momento. Si esto ocurriera, puedo moverme rápidamente a mi escritorio, donde tengo una configuración multipantalla. Puedo hacer clic en la ventana de cualquier moneda y ver rápidamente los detalles completos en una vista de pantalla completa, en la pantalla opuesta. A continuación, se muestra una foto reciente de mí tomada 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 sobre su rostro aterrorizado mira la mesa de operaciones de criptomonedas falsas.
Un pánico, testigo del baño de sangre de YCY

Puedes jugar con la demostración incorporada a continuación o ver su código fuente en caso de error.

Seguridad y permisos

El equipo de Chrome diseñó e implementó la API de Window Management según los principios básicos definidos en Cómo controlar el acceso a funciones potentes de la plataforma web, incluidos el control de 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 creación de huellas digitales de los usuarios, especialmente aquellos con varias pantallas conectadas de manera coherente a sus dispositivos. Como una mitigación de este problema de privacidad, las propiedades de pantalla expuestas se limitan al mínimo necesario para casos de uso de posición comunes. Se requiere el permiso del usuario para que los sitios obtengan información multipantalla y coloquen ventanas en otras pantallas. Si bien Chromium muestra etiquetas de pantalla detalladas, los navegadores tienen la libertad de mostrar etiquetas menos descriptivas (o incluso vacías).

Control de usuarios

El usuario tiene el control total de la exposición de su configuración. Pueden aceptar o rechazar la solicitud de permiso y revocar un permiso otorgado previamente 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 relevante de la configuración de Grupos de políticas atómicos.

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 otorgamientos de permisos. 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 acerca de 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 especificaciones en el repositorio de GitHub correspondiente o agrega tus ideas a un problema existente.

Informar 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?

  • Informa un error en new.crbug.com. Asegúrate de incluir tantos detalles como puedas, además de instrucciones simples para reproducir el contenido, y, luego, ingresa Blink>Screen>MultiScreen en el cuadro Componentes. Glitch funciona muy bien para compartir repros rápidos y fáciles.

Muestra compatibilidad con la API

¿Planeas usar la API de Window Management? Tu apoyo público ayuda al equipo de Chrome a priorizar funciones y les muestra a otros proveedores de navegadores la importancia de brindar compatibilidad.

Vínculos útiles

Agradecimientos

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