Управление несколькими дисплеями с помощью API управления окнами

Получайте информацию о подключенных дисплеях и располагайте окна относительно этих дисплеев.

API управления окнами

API управления окнами позволяет вам перечислять дисплеи, подключенные к вашему компьютеру, и размещать окна на определенных экранах.

Предлагаемые варианты использования

Примеры сайтов, которые могут использовать этот API:

  • Многооконные графические редакторы типа Gimp позволяют размещать различные инструменты редактирования в точно позиционируемых окнах.
  • Виртуальные торговые столы могут отображать рыночные тенденции в нескольких окнах, каждое из которых можно просматривать в полноэкранном режиме.
  • Приложения для показа слайдов могут отображать заметки докладчика на внутреннем основном экране, а презентацию — на внешнем проекторе.

Как использовать API управления окнами

Проблема

Проверенный временем подход к управлению окнами, Window.open() , к сожалению, не поддерживает дополнительные экраны. Хотя некоторые аспекты этого API кажутся несколько архаичными, например, параметр DOMString windowFeatures , он, тем не менее, служил нам верой и правдой на протяжении многих лет. Чтобы указать положение окна, можно передать координаты left и top (или screenX и screenY соответственно), а желаемые размеры width и height (или innerWidth и innerHeight соответственно). Например, чтобы открыть окно размером 400×300 на расстоянии 50 пикселей от левого края и 50 пикселей от верхнего края, можно использовать следующий код:

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

Информацию о текущем экране можно получить, посмотрев на свойство window.screen , которое возвращает объект Screen . Вот вывод на моём MacBook Pro 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
*/

Как и большинству людей, работающих в сфере технологий, мне пришлось адаптироваться к новым реалиям и обустроить свой личный домашний офис. Мой выглядит так, как на фото ниже (если вам интересно, вы можете прочитать полное описание моего офиса ). iPad, стоящий рядом с моим MacBook, подключен к ноутбуку через Sidecar , поэтому при необходимости я могу быстро превратить его во второй экран.

Школьная парта на двух стульях. На парте стоят коробки из-под обуви, а рядом с ними — ноутбук и два iPad.
Многоэкранная конфигурация.

Если я хочу использовать преимущества большего экрана, я могу перенести всплывающее окно из примера кода выше на второй экран. Я делаю это так:

popup.moveTo(2500, 50);

Это грубое предположение, поскольку узнать размеры второго экрана невозможно. Информация из window.screen охватывает только встроенный экран, но не экран iPad. Заявленная width встроенного экрана составляла 1680 пикселей, поэтому изменение до 2500 пикселей может помочь переместить окно на iPad, поскольку я знаю, что он расположен справа на моём MacBook. Как это сделать в общем случае? Оказывается, есть способ лучше, чем угадывать. Это API управления окнами.

Обнаружение особенностей

Чтобы проверить, поддерживается ли API управления окнами, используйте:

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

Разрешение window-management

Прежде чем использовать API управления окнами, я должен запросить у пользователя разрешение на это. Запросить разрешение window-management можно с помощью API разрешений следующим образом:

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

Пока используются браузеры со старым и новым названием разрешения, обязательно используйте защитный код при запросе разрешения, как в примере ниже.

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;
});

Браузер может динамически отображать запрос на разрешение при первой попытке использования любого из методов нового API. Подробнее читайте дальше.

Свойство window.screen.isExtended

Чтобы узнать, подключено ли к моему устройству более одного экрана, я обращаюсь к свойству window.screen.isExtended . Оно возвращает true или false . В моей конфигурации оно возвращает true .

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

Метод getScreenDetails()

Теперь, когда я знаю, что текущая конфигурация — многоэкранная, я могу получить дополнительную информацию о втором экране с помощью Window.getScreenDetails() . Вызов этой функции выведет запрос на разрешение, разрешающее сайту открывать и размещать окна на моём экране. Функция возвращает обещание, которое разрешается объектом ScreenDetailed . На моём MacBook Pro 13 с подключённым iPad это включает поле screens с двумя объектами 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
  }]
}
*/

Информация о подключенных экранах доступна в массиве screens . Обратите внимание, что значение left для iPad начинается со 1680 , что в точности соответствует width встроенного дисплея. Это позволяет мне точно определить логическое расположение экранов (рядом, друг над другом и т. д.). Теперь для каждого экрана также доступны данные о том, является ли он isInternal или isPrimary . Обратите внимание, что встроенный экран не обязательно является основным .

Поле currentScreen — это динамический объект, соответствующий текущему window.screen . Объект обновляется при перемещении окон на другие экраны или при смене устройства.

Событие screenschange

Теперь не хватает только способа отслеживать изменение настроек экрана. Новое событие screenschange делает именно это: оно срабатывает при каждом изменении конфигурации экрана. (Обратите внимание, что слово «screens» стоит во множественном числе в названии события.) Это означает, что событие срабатывает при каждом подключении или отключении нового экрана или существующего (физическом или виртуальном в случае Sidecar).

Обратите внимание, что вам необходимо асинхронно просматривать информацию о новом экране, так как само событие screenschange не предоставляет эти данные. Для просмотра информации об экране используйте живой объект из кэшированного интерфейса Screens .

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;
  }
});

Событие currentscreenchange

Если меня интересуют только изменения текущего экрана (то есть значение живого объекта currentScreen ), я могу прослушивать событие currentscreenchange .

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

Событие change

Наконец, если меня интересуют только изменения конкретного экрана, я могу прослушивать событие change этого экрана.

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

Новые возможности полноэкранного режима

До сих пор можно было запросить отображение элементов в полноэкранном режиме с помощью метода requestFullScreen() с метким названием. Этот метод принимает параметр options , в который можно передать FullscreenOptions . До сих пор единственным его свойством было navigationUI . API управления окнами добавляет новое свойство screen , позволяющее определить, с какого экрана начать полноэкранный режим. Например, если вы хотите сделать основной экран полноэкранным:

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);
}

Полифилл

Невозможно применить полифил для API управления окнами, но вы можете изменить его форму, чтобы иметь возможность писать код исключительно для нового 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;
}

Другие аспекты API, то есть различные события изменения экрана и свойство screen FullscreenOptions , просто никогда не сработают или будут молча игнорироваться браузерами, не поддерживающими их.

Демо

Если вы хоть немного похожи на меня, то вы внимательно следите за развитием различных криптовалют. (На самом деле, я делаю это не потому, что люблю эту планету, но ради этой статьи просто предположу, что я её люблю.) Чтобы отслеживать криптовалюты, которыми я владею, я разработал веб-приложение, которое позволяет мне следить за рынками в любых жизненных ситуациях, например, не вставая с кровати, где у меня есть хороший одноэкранный компьютер.

Огромный экран у изножья кровати, на котором частично видны ноги автора. На экране — фальшивый стол для торговли криптовалютой.
Расслабляюсь и наблюдаю за рынками.

Поскольку речь идёт о криптовалютах, на рынках в любой момент может наступить ажиотаж. В этом случае я могу быстро переместиться к своему столу, где у меня установлено несколько экранов. Я могу нажать на окно любой валюты и быстро увидеть полную информацию в полноэкранном режиме на противоположном экране. Ниже представлена ​​моя недавняя фотография, сделанная во время последней кровавой бойни YCY . Она застала меня врасплох, и я закрыл лицо руками .

Автор, закрыв лицо руками в панике, смотрит на фальшивую площадку для торговли криптовалютой.
В панике наблюдаю за кровавой баней YCY.

Вы можете поиграть с демо-версией , представленной ниже, или посмотреть ее исходный код на GitHub.

Безопасность и разрешения

Команда Chrome разработала и реализовала API управления окнами, используя основные принципы, изложенные в документе «Управление доступом к мощным функциям веб-платформы» , включая пользовательский контроль, прозрачность и эргономику. API управления окнами предоставляет новую информацию об экранах, подключенных к устройству, увеличивая поверхность идентификации пользователей, особенно тех, к чьим устройствам постоянно подключено несколько экранов. В качестве одного из способов решения этой проблемы конфиденциальности раскрываемые свойства экрана ограничены минимумом, необходимым для стандартных вариантов размещения. Для получения сайтами информации о нескольких экранах и размещения окон на других экранах требуется разрешение пользователя. В то время как Chromium возвращает подробные метки экранов, браузеры могут возвращать менее описательные (или даже пустые) метки.

Пользовательский контроль

Пользователь полностью контролирует раскрытие информации о своей настройке. Он может принять или отклонить запрос на разрешение, а также отозвать ранее предоставленное разрешение через функцию информации о сайте в браузере.

контроль предприятия

Пользователи Chrome Enterprise могут контролировать несколько аспектов API управления окнами, как описано в соответствующем разделе настроек групп атомарной политики .

Прозрачность

Факт предоставления разрешения на использование API управления окнами отображается в информации о сайте браузера, а также может быть запрошен через API разрешений.

Сохранение разрешения

Браузер сохраняет предоставленные разрешения. Разрешение можно отозвать через информацию о сайте в браузере.

Обратная связь

Команда Chrome хочет узнать о вашем опыте работы с API управления окнами.

Расскажите нам о дизайне API

Есть ли что-то в API, что работает не так, как вы ожидали? Или отсутствуют методы или свойства, необходимые для реализации вашей идеи? Есть вопросы или комментарии по модели безопасности?

  • Отправьте сообщение о проблеме со спецификацией в соответствующий репозиторий GitHub или добавьте свои мысли к существующей проблеме.

Сообщить о проблеме с реализацией

Вы обнаружили ошибку в реализации Chrome? Или реализация отличается от спецификации?

  • Сообщите об ошибке на сайте new.crbug.com . Опишите её как можно подробнее, предоставьте простые инструкции по воспроизведению ошибки и введите Blink>Screen>MultiScreen в поле «Компоненты» .

Показать поддержку API

Планируете ли вы использовать API управления окнами? Ваша публичная поддержка помогает команде Chrome расставлять приоритеты в отношении функций и показывает другим разработчикам браузеров, насколько важна их поддержка.

  • Поделитесь тем, как вы планируете использовать его, в ветке обсуждения WICG .
  • Отправьте твит @ChromiumDev , используя хэштег #WindowManagement , и расскажите, где и как вы его используете.
  • Попросите других производителей браузеров реализовать API.

Полезные ссылки

Благодарности

Спецификацию API управления окнами редактировали Виктор Костан , Джошуа Белл и Майк Вассерман . API был реализован Майком Вассерманом и Адриенной Уокер . Рецензентами статьи выступили Джо Медли , Франсуа Бофор и Кейс Баскес . Благодарим Лору Торрент Пуиг за фотографии.