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

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

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

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

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

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

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

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

Проблема

Проверенный временем подход к управлению окнами, Window.open() , к сожалению, не знает о дополнительных экранах. Хотя некоторые аспекты этого API кажутся немного архаичными, например, параметр windowFeatures DOMString , он, тем не менее, хорошо служил нам на протяжении многих лет. Чтобы указать положение окна, вы можете передать координаты как 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 во второй экран.

Школьная скамья на двух стульях. На школьной скамье — коробки из-под обуви, на которых стоит ноутбук и два 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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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