跳转到内容

11. API 参考 ​

公共类型命名 ​

Sus* 是产品 API(在 using Sharq.Core 之后调用的 Runtime 与面向买家的 Editor 类型)。 Sharq* 是 .sharq 编译器与流水线(解析器、解释器、导入器)。产品接口使用 ISus*。无前缀的公共类型是封闭的祖父级集合(响应式原语、host/layer 名词,以及这些 API 的配套类型)。不要新增无前缀公共类型;新产品类型用 Sus* / ISus*,新编译器类型用 Sharq*。

SusApp — fluent bootstrap ​

官方文档化的应用入口点。它是 SusBootstrap 之上的一个轻量 fluent 构建器, 保证初始化顺序: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(在 root 与 OverlayHost 上的 UseCustomStyles)
  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 ​

Sharq.Core 中的进程级 gated logger。代理到 UnityEngine.Debug,以便 Editor Console 与游戏内 开发控制台 继续接收消息。不是 SusConsoleService 使用的环形缓冲 SusLogEntry 类型 — 该 struct 只存储被拦截的 Unity 日志,供叠加 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)
脚本定义 SUS_VERBOSE_LOGS初始化时把下限抬到 Verbose;配置与 UseLogLevel 不能再降低

优先级:define 下限 → 最后一次 UseLogLevel / SusLog.Level → 配置中的 logLevel → 默认 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 都会调用,包括之后的每一次;甚至在没有先前 Mounted() 的 detach 时也会调用(在创建的同一帧内被销毁)。
Attached() / Detached()每次 attach / detach 都会调用,二者彼此对称(在仍处于挂载状态时被销毁的实例,收到的 Detached() 会少一次)。Attached() 在 attach 所在的那一帧运行,而非延迟运行。

由此产生的后果:

  • 当 IsRelocating 为 true 时(同一面板内的 detach/attach,例如浮层传送),DisposeAllBindings() 会被跳过,因此在 Created() 中只注册一次的 Watch() / WatchEffect() 会继续工作。在非重定位情况下,真正的 detach 会释放它们。
  • 在 Mounted() 中设置、在 Unmounted() 中拆除的监听器,在其后的第一次 detach 之后将永久失效。请在 Created() 中为组件自身或子元素注册(没有任何东西会拆除它们),而对于必须在重新挂载、传送或重新打开后依然存活的外部来源(模型、服务、同级组件)的订阅,请使用 Attached() / Detached()。
  • Detached() 没有 IsRelocating 豁免:即使是同一面板内的传送,它也会运行,因此它总是与下一次 Attached() 配对。
  • 不要假设 Mounted() 与 Unmounted() 是一一对应的。
csharp
protected override void Attached()  => _model.Changed += OnModelChanged;
protected override void Detached()  => _model.Changed -= OnModelChanged;

Updated() 仅在元素挂载到面板上时运行(OnAttachToPanelHandler)。从构造函数/deferred 调用 ScheduleReactiveUpdates 的做法已被移除——挂载之前 schedule 可能不会触发。

自省(DescribeProps / DescribeAllowed / DescribeEvents) ​

组件在实例上、无需调用方知道其具体类型就能给出的三种只读自我描述。 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() 返回通过 UseAllowed 注册的 prop 的合法值集合—— 取值、fallback、别名——这样调用方无需知道组件使用的具体 SusOptionSets 就能构建选择器。
  • DescribeEvents() 列出公共 On* 委托字段(名称、总线名、payload 类型); SubscribeEvent 以通用方式订阅其中之一,并对参数装箱。

两个特性让数值范围和 prop 依赖变得显式,而不是靠猜:

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(在 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;

标志开启期间:

  • 有限 的 SusMotion play 会立即写入结束值,并同步调用 onComplete, 没有 tick,也没有延迟;
  • 带有 无限 组(Repeat <= 0)的 play 不会启动,目标保持不变;
  • 把标志从 false 切换为 true 会停止所有正在运行的无限 play, 并应用其恢复模式。已在运行的有限 play 会继续播放到结束。

拥有自己循环的组件(SusMotion 之外的定时 ticker)应当监听该标志并 暂停,就像监听其他任何 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