loomgui.com ↗

v0.1.x → v0.2.x

Loom v0.2 is a breaking release with four source changes you make by hand:

  1. Bridge setup. Your [Bridge] class inherits a base class, and a single Loom UI component does the wiring and runtime setup you used to write yourself.
  2. Bridge state API. Observable<T> and ObservableList<T> are gone. State is now plain { get; set; } properties; lists and maps use ReactiveList<T> / ReactiveMap<K,V> (or snapshot collections).
  3. Membership. The [BridgeState] and [BridgeEvent] member attributes are gone. A public property is state and a public Event<T> property is an event by convention. Actions stay explicit: tag each UI-callable method [BridgeAction] (a plain public method is internal, not an action).
  4. Flattened UI surface. The UI reads state and calls actions directly on the bridge: bridge.currentScreen, bridge.addPoint(), instead of the old bridge.state.* / bridge.actions.* split.

What changed at a glance

v0.1.xv0.2.x
Bridge classclass UIBridgeclass UIBridge : LoomBridgeBase
Membership[BridgeState] / [BridgeAction] / [BridgeEvent] tagsstate & events by convention; actions tagged [BridgeAction]
UI bridge accessbridge.state.x / bridge.actions.y()bridge.x / bridge.y()
Constructionnew UIBridge() in your bootstrapLoom constructs it
Registrationyou call RegisterWithLoom()automatic
Runtimeyou call LoomRuntime.Initialize() / Shutdown()automatic
Scene setupa bootstrap MonoBehaviour + Loom Viewone Loom UI component
Bridge accessMyBootstrap.Bridge.XUIBridge.Instance.X
Scalar stateObservable<int> X { get; } = new(0)int X { get; set; }
Read / write stateX.ValueX
List stateObservableList<T>ReactiveList<T> (or snapshot List<T>)
long / ulong in TSnumberbigint
  1. Update the Unity package.

    Update com.loomgui to 0.2.0, then restart the Unity Editor so the new native plugin loads.

  2. Inherit LoomBridgeBase.

    Your [Bridge] class must now inherit LoomBridgeBase.

    [Bridge]
    public partial class UIBridge {
    public partial class UIBridge : LoomBridgeBase {
      // members
    }
  3. Move state from Observable<T> to plain properties.

    A bridge state member is now a plain { get; set; } property. Assigning it pushes to the UI. Remove the Observable<T> wrapper and the .Value accessor everywhere (reads and writes use the property directly), and move the initial value into the property initializer.

    [Bridge]
    public partial class UIBridge : LoomBridgeBase {
      [BridgeState] public Observable<int> Score { get; } = new(0);
      [BridgeState] public int Score { get; set; } = 0;
    
      [BridgeAction] public void AddPoint() { Score.Value += 1; }
      [BridgeAction] public void AddPoint() { Score += 1; }
    }

    If you used Observable<T>.Subscribe(...) for engine-side reactivity, move that logic to where you set the value (a plain property has no Subscribe).

  4. Move lists to ReactiveList<T> (or a snapshot collection).

    ObservableList<T> is replaced by two options:

    • ReactiveList<T> / ReactiveMap<K,V>: declare get-only and mutate in place (Add, Remove, Clear, indexer). Each change pushes granularly.
    • Snapshot List<T> / T[] / Dictionary<K,V>: a { get; set; } property you reassign to sync (mutating in place won’t update the UI).
    [BridgeState] public ObservableList<string> Messages { get; } = new();
    [BridgeState] public ReactiveList<string> Messages { get; } = new();

    See State and collections.

  5. Remove all state/event attributes.

    State and events are now by convention, so the [BridgeState] and [BridgeEvent] attributes are gone. A public property is state and a public Event<T> property is an event. Actions stay explicit: keep [BridgeAction] on every UI-callable method. Delete only the state/event tags:

    [Bridge]
    public partial class UIBridge : LoomBridgeBase {
      [BridgeState] public int Score { get; set; } = 0;
      public int Score { get; set; } = 0;
    
      [BridgeAction] public void AddPoint() { Score += 1; }   // keep, actions stay tagged
    
      [BridgeEvent] public Event<ScoredEvent> Scored { get; } = new();
      public Event<ScoredEvent> Scored { get; } = new();
    }

    [BridgeAction] may also go on a method of a nested state DTO; it surfaces at its dotted path on the bridge (bridge.boot.setProgress(v)). If a state/event member must stay public for other game code but isn’t part of the UI contract, tag it [BridgeIgnore] (or make it non-public). Use [BridgeName("...")] to keep a wire name that no longer matches the member name.

    One footgun the convention introduces: a computed get-only property (public int Doubled => Score * 2;) is now treated as state and won’t update, mark it [BridgeIgnore] or make it a method.

  6. Delete your bootstrap and add the Loom UI component.

    Remove the MonoBehaviour that constructed the bridge and started the runtime. Everything it did is now handled for you:

    // DELETE this whole file (e.g. LoomBootstrap.cs).
    [DefaultExecutionOrder(-1000)]
    public class LoomBootstrap : MonoBehaviour {
      public static UIBridge Bridge { get; private set; }
    
      private void Awake() {
        Bridge = new UIBridge();
        Bridge.RegisterWithLoom();
        LoomRuntime.Initialize(/**/);
      }
    
      private void OnDestroy() => LoomRuntime.Shutdown();
    }

    In your startup scene, run Loom -> Setup UI in Current Scene. This adds a single Loom UI GameObject that builds the canvas, input capture, and ticker and initializes the runtime on Play. Delete the old “Loom Bootstrap” GameObject if you had one.

    See Loom UI for the full component reference.

  7. Update how you reach the bridge.

    The generator now emits a static Instance accessor on your bridge class. Replace references to your old bootstrap singleton, and drop .Value:

    LoomBootstrap.Bridge.Score.Value = 10;
    UIBridge.Instance.Score = 10;
  8. Flatten how the UI reads state and calls actions.

    The UI bridge no longer has a .state / .actions split. State, actions, and nested objects live directly on the bridge. Find-and-replace in your Solid components:

    bridge.state.currentScreen
    bridge.currentScreen
    
    bridge.actions.addPoint()
    bridge.addPoint()

    A nested-DTO action would look like bridge.boot.setProgress(v).

  9. Check long / ulong reads in your UI.

    64-bit integers now cross to TypeScript as bigint (they were number). If your UI reads a long / ulong state field or event payload, use bigint literals (1n), and String(v) to display, since JSON.stringify throws on a bigint. If you only need a large id as text, use a string field instead.

New in v0.2: built-in loom.* state

Every bridge now exposes a read-only loom block (active scene name, viewport size, device pixel ratio, connection status) with no declaration on your part. The UI reads it as bridge.loom.*. It’s additive, so adopting it is optional. See Built-in Loom state.