К содержимому

11. Справочник API ​

Публичные имена типов ​

Sus* — продуктовый API (Runtime и buyer-facing Editor-типы, которые вы вызываете после using Sharq.Core). Sharq* — компилятор и пайплайн .sharq (парсер, интерпретатор, импортёры). Продуктовые интерфейсы — ISus*. Непрефиксные публичные типы — закрытый grandfathered-набор (реактивные примитивы, host- и layer-существительные, компаньоны этих API). Не добавляйте новые непрефиксные публичные типы; новые продуктовые типы — Sus* / ISus*, новые типы компилятора — Sharq*.

SusApp — fluent bootstrap ​

Задокументированная точка входа приложения. Тонкий fluent-билдер над SusBootstrap, который гарантирует порядок инициализации: panel TSS → EventSystem → каскад токенов → OverlayHost → world panel → шрифты → custom styles → configure → mount → тема (последней).

csharp
public sealed class SusApp
{
    public VisualElement Root { get; }
    public UIDocument Document { get; }
    public SusWorldSpacePanel WorldPanel { get; }  // after Run/Mount; null if UseWorldSpace(false)

    // Create
    public static SusApp Create(UIDocument document);   // ApplyDefaultTSS + root
    public static SusApp Create(VisualElement root);    // advanced — no TSS

    // Fluent config (all return this)
    public SusApp UseTheme(SusTheme theme);             // default Dark; applied last
    public SusApp UseTokenCascade(bool enabled = true); // L1–L5 + OverlayHost (default on)
    public SusApp UseWorldSpace(bool enabled = true);   // SusWorldSpacePanel + Default (default on)
    public SusApp UseCustomStyles(params string[] resourcePaths);  // after cascade
    public SusApp UseFonts(SusFontAsset fontAsset);     // writes no style; warns unless exported to USS
    public SusApp UseIcons(params ISusIconProvider[] providers);
    public SusApp UseIcons(SusIconSetAsset iconSet);
    public SusApp UseLogLevel(SusLogLevel level);       // process gate; call before Run/Mount
    public SusApp Configure(Action<VisualElement> configure);  // before theme; router/manual UI

    // Finalize
    public VisualElement Run();                         // no root component
    public T Mount<T>() where T : SusComponent, new();  // Mount + Finalize
}

Порядок Finalize (Run / Mount) ​

  1. Иконки (UseIcons → SusIconRegistry.RegisterProvider)
  2. Каскад токенов (LoadTokenCascade: _palette → _font → _theme → design-tokens → _icon → дополнения + OverlayHost)
  3. World-space панель (EnsureWorldSpacePanel, если в Play Mode и включён UseWorldSpace)
  4. Шрифты (UseFonts — на стиль не влияет; гарнитура берётся из _font.uss, см. Design tokens §2)
  5. Custom styles (UseCustomStyles на root + OverlayHost)
  6. Коллбэки Configure
  7. Монтирование корневого компонента (если Mount<T>)
  8. Тема — последней (SusThemeService.Instance.SetTheme(root, theme))

UseLogLevel не входит в finalize — он сразу выставляет SusLog.Level (безопасно до Run / Mount).

csharp
SusApp.Create(uiDocument)
    .UseTheme(SusTheme.Dark)
    .UseLogLevel(SusLogLevel.Verbose)   // optional — diagnostics / audits
    .UseCustomStyles("SusRuntime/demo-tokens")
    .UseWorldSpace(true)
    .Configure(root => BuildManualUi(root))
    .Mount<HomeScreen>();

// Router apps (sus-router extension):
SusApp.Create(uiDocument)
    .UseTheme(SusTheme.Dark)
    .UseRouter(router, r => r.Register("/", typeof(HomeScreen)), initialPath: "/")
    .Run();

SusLog ​

Процессный gated-логгер в Sharq.Core. Проксирует в UnityEngine.Debug, чтобы Editor Console и игровая dev-консоль продолжали получать сообщения. Не путать с ring-buffer типом SusLogEntry у SusConsoleService — тот struct только хранит перехваченные Unity-логи для overlay UI.

csharp
public enum SusLogLevel
{
    Error = 0,
    Warn = 1,      // default buyer level
    Info = 2,
    Verbose = 3,   // audits, probes, bootstrap traces
}

public static class SusLog
{
    public static SusLogLevel Level { get; set; }   // minimum emitted; default Warn
    public static bool IsEnabled(SusLogLevel level);
    public static bool IsVerbose { get; }           // IsEnabled(Verbose)

    public static void Error(string message);
    public static void Error(string message, Object context);
    public static void Warn(string message);
    public static void Warn(string message, Object context);
    public static void Info(string message);
    public static void Verbose(string message);
    public static void Diagnostic(string message);  // same gate as Verbose
}

По умолчанию: Warn (Error + критичный Warn). Диагностика молчит, пока не поднять уровень.

Как включить Verbose

ИсточникЭффект
SusApp.UseLogLevel(SusLogLevel.Verbose) / SusLog.Level = …Переопределение из кода; вызывать до Run / Mount
Assets/sus.config.json → "logLevel": "Verbose"Читается при первом обращении к SusLog (см. 10-configuration)
Scripting define SUS_VERBOSE_LOGSНа init поднимает пол до Verbose; конфиг и UseLogLevel не могут понизить

Приоритет: пол define → последний UseLogLevel / SusLog.Level → logLevel в конфиге → default Warn.

Для дорогих дампов сначала гейт: if (SusLog.IsVerbose) SusLog.Verbose(...).

SusComponent — базовый класс ​

csharp
public abstract partial class SusComponent : VisualElement
{
    // ─── Creating reactive properties ───
    protected Prop<T> P<T>(T initial = default);
    protected Computed<T> C<T>(Func<T> fn);
    protected WatchHandle Watch<T>(Prop<T> source, Action<T, T> callback);
    protected WatchHandle WatchEffect(Action fn);

    // ─── Life cycle ───
    protected virtual void Created();          // constructor
    protected virtual void BeforeMounted();    // between Created() and Build()
    protected virtual void Mounted();          // ONCE per instance, after Build() - deferred
    protected virtual void Attached();         // EVERY attach to a panel (incl. the first)
    protected virtual void Updated();          // every frame
    protected virtual void BeforeUnmounted();  // BEFORE removing from the panel
    protected virtual void Detached();         // EVERY detach, right before Unmounted()
    protected virtual void Unmounted();        // AFTER detach, on EVERY detach

    // ─── Generation ───
    protected abstract void Build();           // generated by the compiler

    // ─── Provide / Inject ───
    // overwrite:false (default) fires OnDuplicateProvide if the key already exists, then still writes
    protected void Provide<T>(string key, T value, bool overwrite = false);
    protected T Inject<T>(string key);
    protected bool TryInject<T>(string key, out T value);
    protected bool HasInjection(string key);

    // ─── Events ───
    protected void Emit<T>(string eventName, T data);
    public void On(string eventName, Delegate handler);
    public void Off(string eventName, Delegate handler);

    // ─── Slots ───
    protected void RegisterSlotContent(string name, VisualElement content,
        Func<Dictionary<string, object>, VisualElement> builder);   // called from the generated Build()
    protected void BuildSlot(string name, Func<VisualElement, VisualElement> wrapper,
        VisualElement container);
    protected VisualElement GetSlotContainer(string name);
    public VisualElement Slot(string name);                          // runtime access after Build()
}

Жизненный цикл (порядок) ​

Constructor:
  Created() → BeforeMounted() → Build() → LoadCompanionStyleSheets()
Deferred(schedule.Execute, next frame):
  Mounted()
OnAttachToPanel (every attach, including the first):
  ScheduleReactiveUpdates() → Updated() ~60 FPS → Attached()
OnDetachFromPanel (every detach):
  BeforeUnmounted() → _updateItem.Pause() → DisposeAllBindings()* → Detached() → Unmounted()
  (* skipped while IsRelocating)

Асимметрия жизненного цикла: attach один раз, detach каждый раз ​

Mounted() не является зеркалом Unmounted():

ХукКак часто выполняется
Mounted()Один раз на экземпляр — кадр после первого attach. При повторном attach не вызывается.
Unmounted()На каждый detach, включая последующие, и даже detach без предшествующего Mounted() (уничтожен в тот же кадр, в котором создан).
Attached() / Detached()На каждый attach / detach, симметрично друг другу (экземпляр, всё ещё прикреплённый в момент уничтожения, получает на один Detached() меньше). Attached() выполняется в кадре attach, без отложенного вызова.

Следствия:

  • DisposeAllBindings() пропускается, пока IsRelocating равен true (перемещение внутри той же панели, например телепорт оверлея), поэтому Watch() / WatchEffect(), зарегистрированные один раз в Created(), продолжают работать. Вне перемещения настоящий detach их освобождает.
  • Подписка, созданная в Mounted() и снятая в Unmounted(), замолкает навсегда после первого detach, который за ней следует. Регистрируйте обработчики на собственных или дочерних элементах компонента в Created() (их никто не снимает), а для подписок на внешний источник (модель, сервис, соседний компонент), которая должна переживать повторный attach, телепорт или повторное открытие, используйте Attached() / Detached().
  • У Detached() нет исключения для IsRelocating: он выполняется даже при телепорте внутри той же панели, поэтому всегда парен следующему Attached().
  • Не считайте Mounted() и Unmounted() парными один к одному.
csharp
protected override void Attached()  => _model.Changed += OnModelChanged;
protected override void Detached()  => _model.Changed -= OnModelChanged;

Updated() выполняется только когда элемент прикреплён к панели (OnAttachToPanelHandler). Вызов ScheduleReactiveUpdates из конструктора/deferred убран — до attach schedule может не тикать.

Интроспекция (DescribeProps / DescribeAllowed / DescribeEvents) ​

Три read-only ответа, которые компонент даёт о себе на экземпляре, без того чтобы вызывающий код знал его конкретный тип. Из них построена панель контролов storybook — ничего специфичного для этой панели в них нет, поэтому что угодно (UI пресетов сохранения, отладочный инспектор) может вызывать тот же API:

csharp
public partial class SusComponent
{
    public IReadOnlyList<SusPropInfo>                  DescribeProps();
    public IReadOnlyDictionary<string, SusAllowedInfo> DescribeAllowed();
    public IReadOnlyList<SusEventInfo>                 DescribeEvents();
    public IDisposable SubscribeEvent(string eventName, Action<object> handler); // null if unknown
    public bool        IsDependencySatisfied(SusPropInfo prop);
    public string      DescribeDependency(SusPropInfo prop);   // "ContentMode = icon" | null
}
  • DescribeProps() обходит каждое публичное поле Prop<T> / ReadonlyProp<T>: имя, тип значения, текущее значение (читается без регистрации реактивной зависимости), группа, объявленные диапазон/ зависимости, и читал ли или наблюдал его кто-нибудь хоть раз.
  • DescribeAllowed() возвращает набор допустимых значений для prop, зарегистрированного через UseAllowed, — значения, fallback, алиасы, — чтобы вызывающий код мог построить выбор, не зная конкретных SusOptionSets, которые использовал компонент.
  • DescribeEvents() перечисляет публичные поля-делегаты On* (имя, имя в шине, тип payload); SubscribeEvent обобщённо подписывается на одно из них, упаковывая аргумент.

Два атрибута делают числовые диапазоны и зависимости props явными, а не угаданными:

csharp
[SusRange(0, 5, 0.5)]                       public Prop<float>  Rating   = new(0);
[SusRange(0, 100, "%")]                     public Prop<int>    Progress = new(0);
[SusDependsOn(nameof(ContentMode), "icon")] public Prop<string> Icon     = new("");

[SusRange(min, max, step)] (или [SusRange(min, max, unit)]) ограничивает Prop<int>/Prop<float>; без него потребитель откатывается к 0…100. [SusDependsOn(prop, value = null)] — повторяемый, условия объединяются через И — объявляет, что prop имеет значение, только пока другой prop того же компонента держит заданное значение; IsDependencySatisfied / DescribeDependency позволяют вызывающему коду выключить такой контрол и показать почему, вместо того чтобы рисовать контрол, который молча ничего не делает.

Bind-хелперы (реактивные) ​

Все биндинги работают через ReactiveEffect — авто-подписка на Prop<T> / Computed<T> и обновление при изменении любого источника. Каждый хелпер возвращает WatchHandle (отслеживается для dispose при detach).

csharp
// v-if: add/remove from DOM (reactive)
protected WatchHandle BindVisibility(VisualElement el, Func<bool> getter);

// v-show: toggle display (reactive)
protected WatchHandle BindShow(VisualElement el, Func<bool> getter);

// :text: bind string to Label (reactive)
protected WatchHandle BindText(Label label, Func<string> getter);

// :class: switch CSS class by condition (reactive)
protected WatchHandle BindClass(VisualElement el, string className, Func<bool> getter);

// v-for (generic, key-based diff, reactive)
protected WatchHandle BindList<T>(VisualElement container,
    Func<IEnumerable<T>> source,
    Func<T, int, VisualElement> itemBuilder,
    Func<T, object> keySelector = null);

// v-for (generic, IEnumerable - typed item access, reactive)
protected WatchHandle BindListFor<T>(VisualElement container,
    IEnumerable<T> source,
    Func<T, int, VisualElement> itemBuilder,
    Func<T, object> keySelector = null);

v-model (двусторонний биндинг) ​

csharp
// Real BindModel overloads (SusComponent.Bind.cs):
BindModel(myTextField, NameProp);     // TextField      ↔ Prop<string>
BindModel(mySlider, VolumeProp);      // Slider         ↔ Prop<float>
BindModel(myToggle, MuteProp);        // Toggle         ↔ Prop<bool>
BindModel(myDropdown, ModeProp);      // DropdownField  ↔ Prop<string>

Props между компонентами ​

csharp
// Reactive bind on child Prop<T> (:prop="expr" in .sharq)
// case-insensitive, ReactiveEffect, auto-cleanup. Non-generic: getter returns object.
protected void BindChildProp(VisualElement child, string propName, Func<object> getter);

// Direct bind to a Prop<T> instance (old way, PascalCase-sensitive)
protected void BindProperty<T>(Prop<T> target, Func<T> getter);

// Literal prop (prop="value" in .sharq)
// case-insensitive, mutates .Value (does not replace Prop<T>)
internal static void SetChildProp(VisualElement el, string propName, object value);

// Scalar conversion: string→bool/int/float/enum/string
private static object ConvertScalar(object value, Type targetType);

BindList (переупорядочивание с сохранением состояния) ​

csharp
// Insert instead of Remove+Add - focus/scroll/input is not lost
// All three options (BindList<T>, BindListFor, BindListFor<T>) have been fixed.

SusBootstrap ​

csharp
public static class SusBootstrap
{
    // Mounts component T into the container.
    // Loads the design-token cascade in order:
    //   _palette → _font → _theme → design-tokens → _icon → extras + OverlayHost
    // (_global comes from SusDefault.tss / ApplyDefaultTSS — not this cascade.)
    // When called for the first time, automatically creates an EventSystem (no InputModule).
    public static T Mount<T>(VisualElement container) where T : SusComponent, new();
    public static T Mount<T>(UIDocument uiDocument) where T : SusComponent, new();

    // Cascade only (no component) — used by SusApp and manual UI.
    public static void LoadTokenCascade(VisualElement container);

    // Returns or creates the OverlayHost as the last child of the container.
    public static OverlayHost GetOrCreateOverlay(VisualElement container);

    // Finds or creates SusWorldSpacePanel + wires WorldSpaceService.Default
    // (SusApp calls this automatically unless UseWorldSpace(false)).
    public static SusWorldSpacePanel EnsureWorldSpacePanel(
        Camera camera = null, OverlayHost overlayHost = null);

    // Panel TSS (_palette + _font + _global via SusDefault.tss).
    public static void ApplyDefaultTSS(UIDocument document);
}

SusMotion.Reduce — уменьшение движения ​

SusMotion.Reduce — общий для приложения Prop<bool> для настройки доступности «уменьшить движение» (reduce motion). По умолчанию он выключен, и при false каждый SusMotion.Play ведёт себя в точности как раньше.

csharp
// Settings screen: the player asked for less motion.
SusMotion.Reduce.Value = true;

Пока флаг включён:

  • конечный play SusMotion сразу записывает конечные значения и вызывает onComplete синхронно, без тиков и без задержки;
  • play с бесконечной группой (Repeat <= 0) не запускается и не трогает цель;
  • переключение флага с false на true останавливает каждый уже идущий бесконечный play, применяя его режим восстановления. Уже идущие конечные play доигрывают до конца.

Компоненты со своими циклами (запланированные тикеры вне SusMotion) должны следить за флагом и вставать на паузу — так же, как они следят за любым другим Prop<T>:

csharp
SusMotion.Reduce.Changed += (_, reduce) => SetPaused(reduce);
SetPaused(SusMotion.Reduce.Peek());

Подписывайтесь, пока элемент прикреплён, и отписывайтесь при detach: флаг переживает любую панель. USS-переходы это не затрагивает; флаг покрывает непрерывное и скриптовое движение.

Что заменяет (v1 → v2) ​

Старое (v1)Новое (v2)
sus (UPM com.sus.sfc)com.sharq-it.sus.core
sharq-ui-system (SusCompiler.exe, LibSassHost)SharqFileImporter (AssetPostprocessor)
ElementBase — рефлективныйSusComponent : VisualElement
compiled ui/ — смесь ручного и автогенерируемогоgenerated/ — только автоген, .gitignored

Место в экосистеме ​

sus-core (this package)
    ├── sus-router — navigation (Push/Replace/Back, screens)
    └── your Unity project — consumer app