6. Адаптивная раскладка (breakpoints)
Выгода: один .sharq-экран работает на desktop и мобильных без отдельной сборки. Направьте экран на ось breakpoint ниже — его токены перестроят раскладку для узкой панели вместо ручной пересборки под мобильные.
Адаптация под размер экрана использует одну ось: SusBreakpointService.
Нет отдельного сервиса High/Low resolution (SusResolutionService удалён) и нет автоматического масштабирования панели, привязанного к размеру монитора. Density (.density-compact / .density-comfortable) — это ручной пресет продукта, а не адаптация под размер экрана.
PanelSettings у сэмплов используют ConstantPixelSize, поэтому ширина для breakpoint отслеживает ширину панели / Game view (без авто-масштабирования Unity ScaleWithScreenSize).
// Inside SusComponent (injected automatically):
BreakpointService.Current.Value // Prop<Breakpoint>
BreakpointService.IsMobile // Computed<bool> — width ≤ 1024
BreakpointService.IsTablet // Computed<bool> — Md | Lg
BreakpointService.IsDesktop // Computed<bool> — width ≥ 1920Как измеряется ширина
Классы (.breakpoint-*) всегда находятся на корне token cascade (SusBootstrap.TokenCascadeRoot — тот же элемент, что хранит theme/density).
Источник ширины (тот же путь, что использовал старый resolution-сервис):
- Основной:
cascadeRoot.resolvedStyle.width(иlayout.widthпри необходимости) - Fallback: размер
visualTreeпанели - Последний resort: размер Editor Game view /
Screen.width(только когда root ещё не выложен)
Обновления запускаются:
SusBreakpointService.Attach(root)изLoadTokenCascade/Mount- Каждый
GeometryChangedEventотSusComponent→ проталкивает ширину cascade-root (явныйAttach(cascadeRoot).Update(width), тот же паттерн, что у удалённогоSusResolutionService.Update(cascadeRoot, width)) - Лёгкий поллинг + geometry-хук
visualTreeпанели (перетаскивание Editor Game view часто пропускает geometry только на content root)
Не используйте собственную ширину дочернего компонента для выбора класса на root — это некорректно переключит токены для всего дерева.
Совет для Editor
Если Game view зафиксирован на конкретном разрешении (например, 1920×1080), ширина панели может не меняться, даже если вы просто ресайзите закреплённое окно. Используйте Free Aspect и перетаскивайте ширину через пороги ниже, либо принудительно задайте breakpoint в Storybook (select Breakpoint → sm / md / …).
Breakpoints
| Имя | Ширина | Класс на root |
|---|---|---|
Sm | ≤ 640px | .breakpoint-sm |
Md | ≤ 1024px | .breakpoint-md |
Lg | ≤ 1440px | .breakpoint-lg |
Xl | ≤ 1920px | .breakpoint-xl |
Xxl | > 1920px | .breakpoint-2xl |
Токены (--sk-*)
UI-пакеты, построенные на core (kit, game), переопределяют spacing / heights / fonts через .breakpoint-* в собственной таблице токенов. Компоненты уже используют var(--sk-button-height), var(--sk-space-16), var(--sk-font-body) и т.д. — они реагируют автоматически при смене класса на root.
Не все --sk-* токены перемещаются вместе с breakpoint. Размеры разбиты по семействам — spacing, control height, размер шрифта и несколько других переопределяются; border width, radius (кроме pill, который следует за собственной height), letter spacing, 9-slice границы и небольшие оптические коррекции остаются неизменными на каждом breakpoint специально. Полный список семейств и обоснование для каждого инвариантного — см. Design tokens §6.1.
Breakpoint и density (.density-compact / .density-comfortable) оба отвечают на вопрос "сколько здесь места", поэтому проект должен уметь их комбинировать и получать один предсказуемый результат, а не молчаливое переопределение одного другим.
Статус: объединение breakpoint и density предсказуемым образом, и сбор всех комбинаций breakpoint × density в одну сгенерированную таблицу токенов — часть предстоящего выпуска kit — см. changelog kit. Сегодня эти две оси ещё могут конкурировать на некоторых breakpoint; эта заметка будет заменена на простое описание нового поведения как только выпуск выйдет.
Использование в компоненте
public class ResponsivePanel : SusComponent
{
public Prop<float> PanelWidth = new(300f);
protected override void Created()
{
Watch(BreakpointService.Current, (old, bp) =>
{
PanelWidth.Value = bp >= Breakpoint.Xl ? 400f : 300f;
});
}
}USS: переопределение через токен, а не селектор breakpoint
Предпочитайте токен вместо ручного написания селектора .breakpoint-* на проектном классе — токен уже несёт правильное значение для каждого breakpoint, и продолжит работать после разрешения breakpoint и density вместе (см. заметку о статусе выше):
/* Предпочтительно — токен уже варьируется вместе с breakpoint */
.responsive-panel { width: var(--sk-tooltip-max-width, 300px); }/* всё ещё парсится, но переопределение проектного класса такой формы может перестать выигрывать
как только breakpoint и density объединятся с большей специфичностью — переопределите токен вместо этого */
.responsive-panel { width: 300px; }
.breakpoint-xl .responsive-panel { width: 400px; }API
public class SusBreakpointService
{
public Prop<Breakpoint> Current { get; }
public Computed<bool> IsMobile { get; } // ≤ 1024
public Computed<bool> IsTablet { get; } // Md | Lg
public Computed<bool> IsDesktop { get; } // ≥ 1920
/// <summary>When set, width polling is ignored (Storybook / QA).</summary>
public Breakpoint? Override { get; }
public static SusBreakpointService Attach(VisualElement root);
public static SusBreakpointService For(VisualElement root);
public static SusBreakpointService For(SusComponent component);
public void Update(float logicalWidth);
public void UpdateFromElement(VisualElement el);
public void SetOverride(Breakpoint? breakpoint); // null = resume auto
}Attach идемпотентен и является правильной точкой входа (у SusBreakpointService нет .Instance).