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 → тема (последней).
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)
- Иконки (
UseIcons→SusIconRegistry.RegisterProvider) - Каскад токенов (
LoadTokenCascade:_palette→_font→_theme→design-tokens→_icon→ дополнения + OverlayHost) - World-space панель (
EnsureWorldSpacePanel, если в Play Mode и включёнUseWorldSpace) - Шрифты (
UseFonts— на стиль не влияет; гарнитура берётся из_font.uss, см. Design tokens §2) - Custom styles (
UseCustomStylesна root + OverlayHost) - Коллбэки Configure
- Монтирование корневого компонента (если
Mount<T>) - Тема — последней (
SusThemeService.Instance.SetTheme(root, theme))
UseLogLevel не входит в finalize — он сразу выставляет SusLog.Level (безопасно до Run / Mount).
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.
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 — базовый класс
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()парными один к одному.
protected override void Attached() => _model.Changed += OnModelChanged;
protected override void Detached() => _model.Changed -= OnModelChanged;
Updated()выполняется только когда элемент прикреплён к панели (OnAttachToPanelHandler). ВызовScheduleReactiveUpdatesиз конструктора/deferred убран — до attachscheduleможет не тикать.
Интроспекция (DescribeProps / DescribeAllowed / DescribeEvents)
Три read-only ответа, которые компонент даёт о себе на экземпляре, без того чтобы вызывающий код знал его конкретный тип. Из них построена панель контролов storybook — ничего специфичного для этой панели в них нет, поэтому что угодно (UI пресетов сохранения, отладочный инспектор) может вызывать тот же API:
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 явными, а не угаданными:
[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).
// 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 (двусторонний биндинг)
// 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 между компонентами
// 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 (переупорядочивание с сохранением состояния)
// Insert instead of Remove+Add - focus/scroll/input is not lost
// All three options (BindList<T>, BindListFor, BindListFor<T>) have been fixed.SusBootstrap
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 ведёт себя в точности как раньше.
// 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>:
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