Информация об изменениях

Сообщение Re[4]: Верстка UI - обзор подходов от 30.12.2024 19:25

Изменено 30.12.2024 19:41 Alekzander

Re[4]: Верстка UI - обзор подходов
Здравствуйте, Sinclair, Вы писали:

S>То есть декларативщина победила — несмотря на то, что в ней крайне сложно делать вещи, которые чуть менее очевидны.


А можно показать пример того, как "крайне сложно делать вещи, которые чуть менее очевидны"?

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

Элементарно, как ты хендлишь лайаутинг при изменении размеров окна?


Лэйаутинг всегда был проклятием императивных гуёв. Очень часто с ним обходились, как мой тёзка из Македонии: делаем окно no-resize на все времена (т.е. 640×480 или меньше), вот и весь сказ. Иногда контролы растягивали по ширине. Мы все помним эти простыни OnResize() { control1.Move(); control2.Move(); ... control10.Move(); }, с тоннами бойлерплейта. Когда контролов много, логика усложнялась, туда добавляли условия, иногда группировали айдишники (или хендлы, или уж сразу ссылки/указатели). Ладно, если контролам надо было просто подгонять ширину, а если что-то большее?

Допустим, нужно сделать представление с карточками контактов (как в Outlook). Там ведь и переносы на новую строку, и расстояние между карточками, и отступы. И в дело вступает, как я это называю, LISP-архитектура (от старой шутки про то, что любая сложная система без ЛИСПа содержит самодельный ЛИСП, умеющий половину от оригинала, несовместимый и глючный). То есть, люди начинают писать свой layout-менеджер. Самое смешное, что часто люди этого даже не понимают. Им так и кажется, что у них всё императивно. Они начинают... что? Правильно: искать КОМПОНЕНТ. (Кстати, это ещё один антоним декларативности — декларативность с компонентностью не совместима, точнее, у декларативщиков есть только псевдокомпоненты, а не настоящие компоненты, об этом ниже). Компонент, который умеет отображать карточки. А декларативную схему программист закладывает в настройки этого компонента. Когда их сериализуешь — получишь декларативность, но очень хорошо замаскированную. Криптодекларативность. В HTML, для сравнения, не нужен компонент, а нужен простой div. (Или текст, или кнопка, или любой контейнер). Лэйаутинг натягивается на что угодно.

Настоящая проблема (императивщиков) в том, что декларативные средства сейчас стали очень изощрёнными. Даже простая сетка в императивном коде уже превращается в проблему. Там, где в CSS можно указать: grid-template-columns: 2fr 1fr 1fr; и получить сетку с тремя колонками, первая из которых вдвое шире остальных, императивщикам уже надо писать логику с нуля. Некоторые подключают ЧатГПТ, который, как раз, хорошо генерирует решения задач, которые миллион раз уже решали. Но CSS умеет намного больше. Допустим, надо слить в пары две первые и две последних ячейки (в индексах узлов). Или сливать помеченные ячейки. И тут разом приплыли и императивщики, и ЧатГПТ.

Как ты делаешь адаптивность? (Под разные размеры, разные dpi, разный предпочтительный размер шрифта)


Вообще, флексов-гридов-авторазмеров, по идее, достаточно, чтобы сделать адаптивный дизайн под любой экран. Но бывает, что мы хотим радикально поменять расстановку элементов для маленького, среднего и большого размеров. В CSS у нас есть брейкпоинты (они же медиазапросы), которые помогают привязывать способ расстановки к диапазонам. Писать это всё руками — можно застрелиться. Впрочем, те, кто не пишут и не думают ни о какой адаптивности — они, ясен пень, совершенно не страдают.

"Предпочтительный размер шрифта" — раньше это была чисто браузерная хрень:



Но вообще, теперь она везде. В Андроиде, в 11-й винде:



Как её учитывать?

В CSS для этого есть специальные единицы — rem (доли базового размера). Что удобно, дефолтный базовый размер обычно привязывают к степени двойки (16), и это даёт нам всегда конечную десятичную дробь для условно-среднего юзера при переходе от пикселей. Было бы удобнее иметь "условный пиксель", но этого в CSS, увы, нет. Вернее, есть, но не во всех стандартах. Однако, "условный пиксель" можно получить препроцессингом, например, в LESS, если задать нужную переменную, width: 20/@rem; будет означать "ширина 20 пикселей при дефолтном размере, с пропорциональным изменением при недефолтном". А в императивщине что? Это даже сравнивать смешно.

Далее, относительность размеров в CSS иерархическая. R в rem это Root. Но на каждом уровне размеры можно привязать к контейнеру. Допустим, размер иконки 2em это два базовых размера текста для конкретно этого контейнера.

Опять же, если просто не знать про настройку базового размера текста, и игнорировать её, проблема решится сама собой. Для программиста, не для юзера.

Как поддерживаешь no-mouse


Если человек похож на Стивена Хокинга и у него двигается один палец, надо сделать интерфейс доступным для работы с клавиатурой. Впрочем, я просто люблю активно использовать клавиатуру, а не дёргать мышь по пустякам.

Я уже немножко устал писать, поэтому сразу к неочевидным вещам:

/* Special case: if any element OUTSIDE navbar is focused with keyboard, enforce hiding navbar. */
body:has(*:focus-visible) .navbar.headroom--not-top:not(:has(*:focus-visible))
{
    --navbar-action: var(--navbar-dont-mess) !important;
}

/* Special case: if any element INSIDE navbar is focused with keyboard, enforce showing navbar. */
.navbar:has(*:focus-visible)
{
    --navbar-action: var(--navbar-show) !important;
}


Что тут написано? (Это можно было бы упаковать в одно выражение, но я его слегка денормализовал для читаемости). Написано тут следующее. Если пользователь навигирует по скроллируемому контейнеру при помощи кнопок Tab / Shift + Tab (допустим, он редактирует многостраничную презентацию и выделяет объекты внутри слайдов), контейнер, конечно, прокручивается, но выбранный элемент (именно при помощи клавиатуры!) может попасть под навигационную панель. Поэтому, при наличии выделенных с клавиатуры объектов панель должна не мешать. Но если юзер донавигирует до контролов, расположенных на самой панели, она должна быть отображена.

При этом, у нас выдерживается правильный уровень абстрактности. "Не мешать" расшифровывается где-то отдельно. Это может быть простое скрытие, плавное скрытие, полупрозрачность, что угодно.

Я очень хочу посмотреть, как в одну-две строки всю эту логику упихает императивщик.

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

А ещё, декларативный подход позволяет легко и просто задавать разные стилизации фокусных рамок для обычных кнопок, прозрачных кнопок на фоне картинки (например, кнопок управления видеопотоком), светлых, тёмных, всяких разных. ОС (та же винда) в лучшем случае отобразит квадратную рамку с фиксированным офсетом. Которая на круглой кнопке Play/Pause будет смотреться как на корове седло. Для сравнения:



Это стилизация фокусной рамки для круглой прозрачной кнопки на фоне видео. Она, что естественно, такая же круглая, как и сама кнопка.

А вот это нечто похитрее:



Это таб-контрол с переопределениями. В ортодоксальном таб-контроле мы ходим по вкладкам через Tab и активируем их пробелом/Enter'ом. Тут же у нас объединённый контрол, в котором мы переключаемся между вкладками кнопками-стрелками (для удобства). Такие вещи легко переопределять при помощи tabindex: для контейнера он задаёт табабельность (возможность фокусироваться с клавиатуры), а для радиокнопок — её отменяет. При этом, за счёт :has() в селекторе стилизации можно сделать общую фокусную рамку, но оставить старую логику навигации (с переходом от вкладки ко вкладке при помощи Tab).

Как это всё описывать императивно? Со слезами на глазах.

А вот ещё:



Это фокусная рамка (и сам контрол) поменяли свой цвет. Просто потому, что попали на тёмный фон. Декларативное описание в CSS позволяет очень просто группировать цвета (как и всё остальное), делая что угодно от простого "блок на тёмном"/"блок на светлом" до полноценных скинов.

Конечно, если не делать скины, и не делать блоки разноцветными, то проблемы, опять же, не возникнет.

Как поддерживаешь ... слепых


Ну, тут всё просто. Есть декларация — отдаём её скринридеру. Нет декларации — "слепой чёрт должен сидеть дома, а не пользоваться нашими UI'ем!" (ц).

На самом деле, конечно же, дай дураку декларативный HTML/CSS, он и из него сделает императивное говно, и скринридер не сможет ничего озвучить. У декларативности есть свои градации. В HTML истинно декларативный интерфейс называется "семантическая разметка".

Вообще, accessibility это такой тест на вшивость. Если делаешь хорошо (описывая кнопки как кнопки и т.д.), она будет сама собой. Если делаешь плохо, то не будет. Всякие технологии типа WAI ARIA содержат дохрениллион атрибутов, которыми можно управлять accessibility (метка, роль компонента, описание — всё это для скринридера), но искусство программиста в том, чтобы добиваться результата без их использования. Это официальный дзен accessibility, про который можно прочитать на MDN, например.

Конечно, если не поддерживать слепых... Ну, тенденция уже понятна, да?

Как поддерживаешь ... слабовидящих, эпилептиков


"Буду краток". Условные декларативные стилизации, собранные в одном месте, позволяют добиться результата намного проще, чем куча if'ов, разбросанных по всему коду.

Как что-то меняешь в чужих контролах?


Был такой случай. Чел спросил: я, мол, пользуюсь слайдером Owl carousel (известный слайдер) и хочу задать промежуток адаптивно (т.е. не 10px, а 0.625rem, чтобы при изменении базового размера промежуток пропорционально менялся). А компонент принимает строго число, трактуя его как пиксели. Как быть?

Императивно-компонентный подход настолько въелся в этих людей, что они не видят совершенно очевидного. Тут нет компонентов. А эти псевдокомпоненты (я обещал, что ниже объясню) — всего лишь декларативная разметка, декларативная стилизация и немножко обработчиков.

Ему ответили совершенно правильно: посмотри, какую разметку генерирует "компонент", и перестилизуй её как хочешь. Грубо говоря, напиши:

.owl-slide
{
    margin: 0.625rem !important;
}


Что делают при отсутствии декларативности? Покупают исходный код компонента, меняют, пересобирают. Иногда декомпилируют. При каждом обновлении версии — смыть, повторить.

А ведь в чужих компонентах бывают ошибки и похлеще.

Поисправляешь их недельку-другую, и быстро начнёшь ценить декларативность.

И чем больше у тебя юзеров, тем больше среди них слепых, одноруких, эпилептиков, тех, кто поставил размер базового шрифта ровно 21, юзает вертикальный экран с HiDPI и коэффициентом масштабирования 125%, и ещё выводит результат на печать. И приходится всё делать правильно, потому что каждая группа это, например, тысяча лицензий.
Re[4]: Верстка UI - обзор подходов
Здравствуйте, Sinclair, Вы писали:

S>То есть декларативщина победила — несмотря на то, что в ней крайне сложно делать вещи, которые чуть менее очевидны.


А можно показать пример того, как "крайне сложно делать вещи, которые чуть менее очевидны"?

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

Элементарно, как ты хендлишь лайаутинг при изменении размеров окна?


Лэйаутинг всегда был проклятием императивных гуёв. Очень часто с ним обходились, как мой тёзка из Македонии: делаем окно no-resize на все времена (т.е. 640×480 или меньше), вот и весь сказ. Иногда контролы растягивали по ширине. Мы все помним эти простыни OnResize() { control1.Move(); control2.Move(); ... control10.Move(); }, с тоннами бойлерплейта. Когда контролов много, логика усложнялась, туда добавляли условия, иногда группировали айдишники (или хендлы, или уж сразу ссылки/указатели). Ладно, если контролам надо было просто подгонять ширину, а если что-то большее?

Допустим, нужно сделать представление с карточками контактов (как в Outlook). Там ведь и переносы на новую строку, и расстояние между карточками, и отступы. И в дело вступает, как я это называю, LISP-архитектура (от старой шутки про то, что любая сложная система без ЛИСПа содержит самодельный ЛИСП, умеющий половину от оригинала, несовместимый и глючный). То есть, люди начинают писать свой layout-менеджер. Самое смешное, что часто люди этого даже не понимают. Им так и кажется, что у них всё императивно. Они начинают... что? Правильно: искать КОМПОНЕНТ. (Кстати, это ещё один антоним декларативности — декларативность с компонентностью не совместима, точнее, в HTML (как высшей форме декларативности) есть только псевдокомпоненты, а не настоящие компоненты, об этом ниже). Компонент, который умеет отображать карточки. А декларативную схему программист закладывает в настройки этого компонента. Когда их сериализуешь — получишь декларативность, но очень хорошо замаскированную. Криптодекларативность. В HTML, для сравнения, не нужен компонент, а нужен простой div. (Или текст, или кнопка, или любой контейнер). Лэйаутинг натягивается на что угодно.

Настоящая проблема (императивщиков) в том, что декларативные средства сейчас стали очень изощрёнными. Даже простая сетка в императивном коде уже превращается в проблему. Там, где в CSS можно указать: grid-template-columns: 2fr 1fr 1fr; и получить сетку с тремя колонками, первая из которых вдвое шире остальных, императивщикам уже надо писать логику с нуля. Некоторые подключают ЧатГПТ, который, как раз, хорошо генерирует решения задач, которые миллион раз уже решали. Но CSS умеет намного больше. Допустим, надо слить в пары две первые и две последних ячейки (в индексах узлов). Или сливать помеченные ячейки. И тут разом приплыли и императивщики, и ЧатГПТ.

Как ты делаешь адаптивность? (Под разные размеры, разные dpi, разный предпочтительный размер шрифта)


Вообще, флексов-гридов-авторазмеров, по идее, достаточно, чтобы сделать адаптивный дизайн под любой экран. Но бывает, что мы хотим радикально поменять расстановку элементов для маленького, среднего и большого размеров. В CSS у нас есть брейкпоинты (они же медиазапросы), которые помогают привязывать способ расстановки к диапазонам. Писать это всё руками — можно застрелиться. Впрочем, те, кто не пишут и не думают ни о какой адаптивности — они, ясен пень, совершенно не страдают.

"Предпочтительный размер шрифта" — раньше это была чисто браузерная хрень:



Но вообще, теперь она везде. В Андроиде, в 11-й винде:



Как её учитывать?

В CSS для этого есть специальные единицы — rem (доли базового размера). Что удобно, дефолтный базовый размер обычно привязывают к степени двойки (16), и это даёт нам всегда конечную десятичную дробь для условно-среднего юзера при переходе от пикселей. Было бы удобнее иметь "условный пиксель", но этого в CSS, увы, нет. Вернее, есть, но не во всех стандартах. Однако, "условный пиксель" можно получить препроцессингом, например, в LESS, если задать нужную переменную, width: 20/@rem; будет означать "ширина 20 пикселей при дефолтном размере, с пропорциональным изменением при недефолтном". А в императивщине что? Это даже сравнивать смешно.

Далее, относительность размеров в CSS иерархическая. R в rem это Root. Но на каждом уровне размеры можно привязать к контейнеру. Допустим, размер иконки 2em это два базовых размера текста для конкретно этого контейнера.

Опять же, если просто не знать про настройку базового размера текста, и игнорировать её, проблема решится сама собой. Для программиста, не для юзера.

Как поддерживаешь no-mouse


Если человек похож на Стивена Хокинга и у него двигается один палец, надо сделать интерфейс доступным для работы с клавиатурой. Впрочем, я просто люблю активно использовать клавиатуру, а не дёргать мышь по пустякам.

Я уже немножко устал писать, поэтому сразу к неочевидным вещам:

/* Special case: if any element OUTSIDE navbar is focused with keyboard, enforce hiding navbar. */
body:has(*:focus-visible) .navbar.headroom--not-top:not(:has(*:focus-visible))
{
    --navbar-action: var(--navbar-dont-mess) !important;
}

/* Special case: if any element INSIDE navbar is focused with keyboard, enforce showing navbar. */
.navbar:has(*:focus-visible)
{
    --navbar-action: var(--navbar-show) !important;
}


Что тут написано? (Это можно было бы упаковать в одно выражение, но я его слегка денормализовал для читаемости). Написано тут следующее. Если пользователь навигирует по скроллируемому контейнеру при помощи кнопок Tab / Shift + Tab (допустим, он редактирует многостраничную презентацию и выделяет объекты внутри слайдов), контейнер, конечно, прокручивается, но выбранный элемент (именно при помощи клавиатуры!) может попасть под навигационную панель. Поэтому, при наличии выделенных с клавиатуры объектов панель должна не мешать. Но если юзер донавигирует до контролов, расположенных на самой панели, она должна быть отображена.

При этом, у нас выдерживается правильный уровень абстрактности. "Не мешать" расшифровывается где-то отдельно. Это может быть простое скрытие, плавное скрытие, полупрозрачность, что угодно.

Я очень хочу посмотреть, как в одну-две строки всю эту логику упихает императивщик.

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

А ещё, декларативный подход позволяет легко и просто задавать разные стилизации фокусных рамок для обычных кнопок, прозрачных кнопок на фоне картинки (например, кнопок управления видеопотоком), светлых, тёмных, всяких разных. ОС (та же винда) в лучшем случае отобразит квадратную рамку с фиксированным офсетом. Которая на круглой кнопке Play/Pause будет смотреться как на корове седло. Для сравнения:



Это стилизация фокусной рамки для круглой прозрачной кнопки на фоне видео. Она, что естественно, такая же круглая, как и сама кнопка.

А вот это нечто похитрее:



Это таб-контрол с переопределениями. В ортодоксальном таб-контроле мы ходим по вкладкам через Tab и активируем их пробелом/Enter'ом. Тут же у нас объединённый контрол, в котором мы переключаемся между вкладками кнопками-стрелками (для удобства). Такие вещи легко переопределять при помощи tabindex: для контейнера он задаёт табабельность (возможность фокусироваться с клавиатуры), а для радиокнопок — её отменяет. При этом, за счёт :has() в селекторе стилизации можно сделать общую фокусную рамку, но оставить старую логику навигации (с переходом от вкладки ко вкладке при помощи Tab).

Как это всё описывать императивно? Со слезами на глазах.

А вот ещё:



Это фокусная рамка (и сам контрол) поменяли свой цвет. Просто потому, что попали на тёмный фон. Декларативное описание в CSS позволяет очень просто группировать цвета (как и всё остальное), делая что угодно от простого "блок на тёмном"/"блок на светлом" до полноценных скинов.

Конечно, если не делать скины, и не делать блоки разноцветными, то проблемы, опять же, не возникнет.

Как поддерживаешь ... слепых


Ну, тут всё просто. Есть декларация — отдаём её скринридеру. Нет декларации — "слепой чёрт должен сидеть дома, а не пользоваться нашими UI'ем!" (ц).

На самом деле, конечно же, дай дураку декларативный HTML/CSS, он и из него сделает императивное говно, и скринридер не сможет ничего озвучить. У декларативности есть свои градации. В HTML истинно декларативный интерфейс называется "семантическая разметка".

Вообще, accessibility это такой тест на вшивость. Если делаешь хорошо (описывая кнопки как кнопки и т.д.), она будет сама собой. Если делаешь плохо, то не будет. Всякие технологии типа WAI ARIA содержат дохрениллион атрибутов, которыми можно управлять accessibility (метка, роль компонента, описание — всё это для скринридера), но искусство программиста в том, чтобы добиваться результата без их использования. Это официальный дзен accessibility, про который можно прочитать на MDN, например.

Конечно, если не поддерживать слепых... Ну, тенденция уже понятна, да?

Как поддерживаешь ... слабовидящих, эпилептиков


"Буду краток". Условные декларативные стилизации, собранные в одном месте, позволяют добиться результата намного проще, чем куча if'ов, разбросанных по всему коду.

Как что-то меняешь в чужих контролах?


Был такой случай. Чел спросил: я, мол, пользуюсь слайдером Owl carousel (известный слайдер) и хочу задать промежуток адаптивно (т.е. не 10px, а 0.625rem, чтобы при изменении базового размера промежуток пропорционально менялся). А компонент принимает строго число, трактуя его как пиксели. Как быть?

Императивно-компонентный подход настолько въелся в этих людей, что они не видят совершенно очевидного. Тут нет компонентов. А эти псевдокомпоненты (я обещал, что ниже объясню) — всего лишь декларативная разметка, декларативная стилизация и немножко обработчиков.

Ему ответили совершенно правильно: посмотри, какую разметку генерирует "компонент", и перестилизуй её как хочешь. Грубо говоря, напиши:

.owl-slide
{
    margin: 0.625rem !important;
}


Что делают при отсутствии декларативности? Покупают исходный код компонента, меняют, пересобирают. Иногда декомпилируют. При каждом обновлении версии — смыть, повторить.

А ведь в чужих компонентах бывают ошибки и похлеще.

Поисправляешь их недельку-другую, и быстро начнёшь ценить декларативность.

И чем больше у тебя юзеров, тем больше среди них слепых, одноруких, эпилептиков, тех, кто поставил размер базового шрифта ровно 21, юзает вертикальный экран с HiDPI и коэффициентом масштабирования 125%, и ещё выводит результат на печать. И приходится всё делать правильно, потому что каждая группа это, например, тысяча лицензий.