Administra varias pantallas con la API de Window Management

Obtén información sobre las pantallas conectadas y posiciona las 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 al estilo de Gimp pueden colocar varias herramientas de edición en ventanas posicionadas con precisión.
  • Las mesas de operaciones virtuales pueden mostrar las tendencias del mercado en varias ventanas, cada una de las cuales se puede ver en modo de pantalla completa.
  • Las apps 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 para controlar ventanas, Window.open(), no es consciente de las pantallas adicionales. Si bien algunos aspectos de esta API parecen un poco arcaicos, como su parámetro windowFeatures DOMString, nos ha servido bien 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, este es el código que podrías usar:

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

Para obtener información sobre la pantalla actual, consulta 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 en casa personal. La mía se ve como en la foto de abajo (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 con dos sillas. En la parte superior del 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. Lo hago así:

popup.moveTo(2500, 50);

Esta es una suposició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 pantalla del iPad. El width informado de la pantalla integrada era de 1680 píxeles, por lo que pasar a 2500 píxeles podría funcionar para mover 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 forma mejor que adivinar. Esa forma es la API de Window Management.

Detección de atributos

Para comprobar si la API de Window Management es compatible, 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 los 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 elegir 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 más de una pantalla está conectada a mi dispositivo, accedo a la propiedad window.screen.isExtended. Muestra true o false. En 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 llamo a esta función, se mostrará un mensaje de permiso que me pregunta 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 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 o 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 la pantalla. Un evento nuevo, screenschange, hace exactamente eso: se activa cada vez que se modifica la constelación de la pantalla. (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 (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 no proporciona estos datos. Para buscar los detalles de la pantalla, usa el objeto en vivo 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 currentScreen en vivo), 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 el modo de pantalla completa a través del método apropiadamente llamado requestFullScreen(). El método toma un parámetro options en el que puedes pasar FullscreenOptions. Hasta ahora, 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 sea 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 realizar un polyfill de la API de Window Management, pero puedes usar un shim para su forma para que puedas codificar exclusivamente para 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án o se ignorarán de forma silenciosa, respectivamente, en los navegadores que no son compatibles.

Demostración

Si eres como yo, te mantienes al tanto del desarrollo de las diversas criptomonedas. (En realidad, no lo hago porque amo este planeta, pero, por el bien de este artículo, supongamos que sí). Para hacer un seguimiento de las criptomonedas que tengo, desarrollé una app web que me permite ver 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.

Pantalla de TV enorme al final de una cama con las piernas del autor parcialmente visibles. En la pantalla, se ve un escritorio falso de comercio de criptomonedas.
Relajarme y observar los mercados.

Como se trata de criptomonedas, los mercados pueden ser muy activos en cualquier momento. Si esto sucede, puedo moverme 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 los detalles completos en una vista de pantalla completa en la pantalla opuesta. A continuación, se muestra una foto reciente de mi toma durante el último baño de sangre de YCY. Me tomó completamente desprevenida y me dejó con las manos en la cara.

El autor con las manos en el rostro, asustado, mirando el escritorio de comercio de criptomonedas falso.
Temor, presenciar el baño de sangre de YCY.

Puedes jugar con la demo incorporada a continuación o ver su código fuente en glitch.

Seguridad y permisos

El equipo de Chrome diseñó e implementó la API de Window Management con los principios básicos 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, aquellos que tienen varias pantallas conectadas de forma coherente a sus dispositivos. Como una mitigación de esta preocupación por la privacidad, las propiedades de pantalla expuestas se limitan al mínimo necesario para los casos de uso de posiciones comunes. Se requiere el permiso del usuario para que los sitios obtengan información de varias pantallas y coloquen ventanas en otras pantallas. Si bien Chromium muestra etiquetas de pantalla detalladas, los navegadores pueden 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 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 relevante de la configuración de los grupos de políticas atómicas.

Transparencia

El hecho de si se otorgó 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 tus experiencias con la API de Window Management.

Cuéntanos sobre el diseño de la API

¿Hay algo en 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 comentarios a un problema existente.

Denuncia 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 sea posible, instrucciones simples para reproducirlo y, luego, ingresa Blink>Screen>MultiScreen en el cuadro Components. Glitch es excelente para compartir reproducciones rápidas y fáciles.

Cómo mostrar compatibilidad con la API

¿Piensas usar la API de Window Management? Tu apoyo público ayuda al equipo de Chrome a priorizar las funciones y les 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. Mike Wasserman y Adrienne Walker implementaron la API. Joe Medley, François Beaufort y Kayce Basques revisaron este artículo. Agradecemos a Laura Torrent Puig por las fotos.