6. Адаптивная вёрстка (breakpoints)
Выгода: один .sharq-экран едет на desktop и mobile без отдельной сборки. Направьте экран на ось breakpoint ниже — токены перестроят layout под узкую панель, вместо того чтобы руками пересобирать экран под mobile.
Адаптация под размер экрана использует одну ось: 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-*)
Downstream UI-пакеты переопределяют spacing / heights / fonts под .breakpoint-* (например, downstream-таблица *-tokens.uss). Компоненты уже используют var(--sk-button-height), var(--sk-space-16), var(--sk-font-body) и т.д. — они реагируют автоматически при смене класса на root.
Блоки токенов breakpoint идут после density в таблице токенов kit, поэтому пересекающиеся ключи (heights, spacing) отдают предпочтение активному 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
.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).