com.openugd.corelib.widgets binds Unity's uGUI controls (Button, Toggle, Slider, Text, Image,
RawImage, TMP_Text, TMP_InputField) to models through small presenters that never leave a listener behind.
Use it when you compose screens with the OpenUGD presenter tree from
com.openugd.corelib and want buttons, toggles, sliders, labels and
input fields wired to a scope instead of by hand.
Each presenter is handed a view it does not create, adds its Unity listener for as long as that view is attached, renders its model without ever reporting the render as a change, and reports what the user did through a lifetime-scoped signal. The package also has a tap-and-swipe detector, clickable TextMeshPro links, a presenter that drives any model from a timer, and optional localisation for every text presenter.
Why the ID says widgets.
com.openugd.corelib.widgetsnames what the presenters bind to, Unity's widgets; the types are named for what they are, presenters.
openupm add com.openugd.corelib.widgets@2.0.0Add the OpenUPM registry and the package to Packages/manifest.json:
{
"scopedRegistries": [
{
"name": "package.openupm.com",
"url": "https://package.openupm.com",
"scopes": [
"com.openugd"
]
}
],
"dependencies": {
"com.openugd.corelib.widgets": "2.0.0"
}
}UPM does not resolve the OpenUGD dependencies of a git package from OpenUPM, so list the family packages this one needs as well:
{
"dependencies": {
"com.openugd.lifetime": "https://github.com/openugd/upm-lifetime.git#2.0.0",
"com.openugd.signal": "https://github.com/openugd/upm-signal.git#2.0.0",
"com.openugd.context": "https://github.com/openugd/upm-context.git#2.0.0",
"com.openugd.corelib": "https://github.com/openugd/upm-corelib.git#2.0.0",
"com.openugd.corelib.widgets": "https://github.com/openugd/upm-corelib-widgets.git#2.0.0"
}
}com.unity.ugui comes from Unity's own registry and needs no entry.
- Unity 6000.0 or newer.
com.unity.ugui2.0.0. On Unity 6 it contains TextMeshPro as theUnity.TextMeshProassembly, which the TMP presenters,InputFieldPresenterandHyperlinkTextuse. The package does not depend oncom.unity.textmeshpro, which on Unity 6 is a deprecated shim. To draw TextMeshPro text a project still needs the TMP Essential Resources (Window > TextMeshPro > Import TMP Essential Resources).com.openugd.corelib,com.openugd.context,com.openugd.lifetimeandcom.openugd.signal2.0.0.
Asmdef references are not transitive. An assembly definition of yours that uses these presenters references
com.openugd.corelib.widgets and com.openugd.presenters, plus com.openugd.lifetime and com.openugd.signal
to pass a Lifetime or subscribe to a signal, com.openugd.context for Context, ContextBuilder or
[Inject], com.openugd.corelib for ContextBehaviour, ContextPresenterFactory or ICoroutineProvider, and
Unity.TextMeshPro for the TMP types. Scripts in Assembly-CSharp see everything.
A presenter tree needs a Lifetime, which says when it ends, and an IPresenterFactory, which injects it.
ContextPresenterFactory injects from an OpenUGD.Context, which a ContextBehaviour from
com.openugd.corelib builds. Put this component on a GameObject
in a scene that has a Canvas and an EventSystem, assign the six controls, and press Play.
using System.Threading;
using System.Threading.Tasks;
using OpenUGD;
using OpenUGD.Core;
using OpenUGD.Presenters;
using UnityEngine;
using UnityEngine.UI;
public sealed class SettingsScreen : ContextBehaviour
{
[SerializeField] private Text _title;
[SerializeField] private Text _score;
[SerializeField] private Image _icon;
[SerializeField] private Button _accept;
[SerializeField] private Toggle _sound;
[SerializeField] private Slider _volume;
protected override bool PersistAcrossScenes => false;
// The context the presenters are injected from. They need nothing from it; a game registers its services
// here, an ILocalization among them.
protected override Task<Context> CreateContextAsync(CancellationToken cancellationToken) =>
Context.CreateBuilder(Lifetime).BuildAsync(cancellationToken);
protected override void OnStarted(Context context)
{
// The tree ends with this component's Lifetime, in OnDestroy or when Play mode stops, and every listener
// below is removed with it. There is no unsubscribe code anywhere.
var root = new Presenter.Root(Lifetime, new ContextPresenterFactory(context));
root.AddText(_title, "Settings");
var score = root.AddText(_score, "Score: {0}", 0);
root.AddImage(_icon, null);
var accept = root.AddButton(_accept, () => Debug.Log("accepted"));
accept.Clicked.Subscribe(Lifetime, () => Debug.Log("also accepted"));
root.AddToggle(_sound, new ToggleModel(on => Debug.Log($"sound: {on}"), true));
var volume = root.AddSliderFloat(_volume, 0.5f);
volume.ValueChanged.Subscribe(Lifetime, value => Debug.Log($"volume: {value}"));
// An update is a new model: the presenter renders it, and reports nothing.
score.SetModel(new TextModel { Format = "Score: {0}", Keys = new object[] { 100 } });
volume.SetModel(0.8f);
}
}Lifetime comes from com.openugd.lifetime: a scope you
register clean-up on with AddAction. When it ends, the clean-up runs once, in reverse order of registration, and
every scope made from it with DefineNested ends with it. The signals come from
com.openugd.signal: every Subscribe takes the subscriber's
lifetime, so a handler is removed when the subscriber or the signal's owner ends, with no unsubscribe call.
Each Add… helper attaches the presenter under its parent (AddPresenter), sets the model, then sets the view, so
the presenter renders once with both in place. A null view is allowed: the presenter renders and listens once a
view is set. The helper returns the presenter; keep it to update it later with SetModel, to read its signals, or
to close it early. There are no Update… helpers.
A render writes the widget with Unity's …WithoutNotify setters, and the int slider and the input field also
ignore what the widget raises while they render. So a model callback or a signal only ever reports the user, or
code that writes the widget directly, and code that calls SetModel never needs to filter out its own echo. A
null model is safe everywhere: a toggle or an int slider is left as it is, a text renders empty, an input field
empties, a raw image is disabled, an image loses its sprite.
Every input presenter reports its changes through a subscribe-only ISignal property: ButtonPresenter.Clicked,
TogglePresenter.Toggled, SliderFloatPresenter.ValueChanged, SliderIntPresenter.ValueChanged and
InputFieldPresenter.ValueChanged. A signal is created on first read and ends with the presenter; a subscription
ends with the subscriber's lifetime or the presenter, whichever comes first. Reading a signal before the presenter
is attached throws InvalidOperationException. Where the model also carries a callback (ButtonPresenter's
Action, ToggleModel.OnChanged, SliderIntModel.OnValueChanged), the callback runs first.
Each presenter that listens to its view adds the listener in OnViewAdded, on the view's own ViewLifetime.
That scope ends when the view is replaced, detached or the presenter closes, so the listener goes with it:
replacing a view moves the listener, detaching removes it, and re-attaching adds exactly one. That is what makes
recycled views safe. A list can keep one presenter per item and move a pooled row view between them:
using OpenUGD.Presenters;
using UnityEngine.UI;
public static class RowRecycling
{
// The row's button now calls the second item's handler only. The first presenter's listener left with the
// view, so it never fires for the wrong item, and never twice.
public static void Move(ButtonPresenter from, ButtonPresenter to, Button row)
{
from.SetView(null);
to.SetView(row);
}
}The Recycled List sample does this for a whole list.
TextPresenter (legacy Text), TMPPresenter (TMP_Text) and HyperlinkTextPresenter render a TextModel: a
text or localisation key in Format, and optional arguments in Keys. A string converts to a TextModel, so
AddText(label, "Hello") and label.SetModel("Hello") work. TextModel.Resolve(localization) is the one rule all
three render by:
- without a localisation,
Formatas it is, orstring.Format(Format, Keys)when there are arguments; - with one,
Formatis translated, and everystringargument is translated as a key of its own before it is formatted. Other arguments are formatted as they are, so pass text that is not a key, such as a player's name, as an object that is not astring.
Nothing in the OpenUGD packages implements ILocalization. Register your own in the context the tree is injected
from, and register an ILocalizationChanged too if the language can change while the game runs. Both are
optional and injected with [Inject(Optional = true)].
using System.Collections.Generic;
using OpenUGD;
using OpenUGD.Presenters;
public sealed class DictionaryLocalization : ILocalization
{
private Dictionary<string, string> _texts = new Dictionary<string, string>();
public void Load(Dictionary<string, string> texts) => _texts = texts;
// Return the key for an unknown key, so a missing translation shows on screen.
public string Get(string key) => _texts.TryGetValue(key, out var text) ? text : key;
}
public sealed class LanguageChanged : Signal, ILocalizationChanged
{
public LanguageChanged(Lifetime lifetime) : base(lifetime)
{
}
}
public static class LocalizationInstaller
{
public static void Install(ContextBuilder builder, DictionaryLocalization texts, LanguageChanged changed)
{
builder.Services.AddInstance<ILocalization>(texts);
builder.Services.AddInstance<ILocalizationChanged>(changed);
}
}Switch the language, then fire the signal: texts.Load(french); changed.Fire(); renders every text presenter in
the tree again. For a text widget the package does not cover, derive from TextModelPresenter<TView> and
implement Render(string); localisation and re-rendering come with it:
using OpenUGD.Presenters;
using UnityEngine;
public sealed class TextMeshPresenter : TextModelPresenter<TextMesh>
{
protected override void Render(string text) => View.text = text;
}WithIntervalUpdate sets a presenter's model from a callback on a timer, for a value with no change event: a
countdown, a clock. It runs in unscaled time, so a paused game does not freeze it, and it stops when the presenter
closes or the callback disposes the scope it receives. It takes the coroutine runner as a parameter: inject an
ICoroutineProvider into the presenter that calls it (ContextBehaviour is one, registered with
AddInstance<ICoroutineProvider>(this)).
using System;
using OpenUGD.Presenters;
using OpenUGD.Utils;
using TMPro;
public static class Countdown
{
// Shows the time left until `end`, once a second, until it reaches zero or the label's presenter closes.
public static TMPPresenter Show(Presenter parent, TMP_Text label, DateTime end, ICoroutineProvider coroutines)
{
var presenter = parent.AddText(label, "");
presenter.WithIntervalUpdate(coroutines, scope =>
{
var left = end - DateTime.UtcNow;
if (left > TimeSpan.Zero) return left.ToString(@"mm\:ss");
scope.Dispose();
return "Done";
}, TimeSpan.FromSeconds(1));
return presenter;
}
}UIGestureDetector turns the pointer events on a UI element into taps and four-way swipes, published as
ISignals. Put it on a raycast target (a transparent Image makes an invisible area). AddGesture funnels the
five signals into one callback:
using OpenUGD.Presenters;
using OpenUGD.UI;
using UnityEngine;
public static class CardGestures
{
public static GesturePresenter Bind(Presenter parent, UIGestureDetector card) =>
parent.AddGesture(card, (sender, gesture) =>
{
if (gesture == Gesture.Left) Debug.Log("dismissed");
else if (gesture == Gesture.Tap) Debug.Log("opened");
});
}A press reports at most one gesture. A swipe is a movement past swipeThresholdOfScreen (a fraction of the screen
height, default 0.1); it fires as the pointer crosses it, or on release with detectSwipeOnlyAfterRelease. A tap is
a release within that distance of the press, by a pointer that never went further. The detector follows the
pointer outside the element, and takes the drags of its own presses from a ScrollRect above it. Its signals
accept subscriptions before the GameObject is first active, and a detector that is never activated leaves nothing
behind.
HyperlinkText goes on a TextMeshPro label and makes its <link="id"> tags clickable. A click raises
LinkClicked first; unless a handler marks it handled, or OpenUrls is off, the id is then opened with
Application.OpenURL and HyperlinkOpenEvent reports it. The id is opened as written, so check ids you did not
author:
using System;
using OpenUGD.UI;
public static class SafeLinks
{
public static void OnlyHttps(HyperlinkText links) =>
links.LinkClicked += click =>
{
if (!click.LinkId.StartsWith("https://", StringComparison.Ordinal)) click.Handled = true;
};
}These two are C# events, not signals: remove your handler when its owner goes, for example with
lifetime.AddAction(() => links.LinkClicked -= handler). HyperlinkTextPresenter renders a TextModel into the
label, localised like the other text presenters.
The presenters, all in OpenUGD.Presenters:
| Type | View | Model | Added with | Reports changes through |
|---|---|---|---|---|
ButtonPresenter |
Button |
Action |
parent.AddButton(view, listener) |
the model, then Clicked |
TogglePresenter |
Toggle |
ToggleModel |
parent.AddToggle(view, model); RegisterToggleInGroup(group) |
ToggleModel.OnChanged, then Toggled |
SliderFloatPresenter |
Slider |
float (NaN leaves the slider alone) |
parent.AddSliderFloat(view[, value][, onChange]) |
ValueChanged |
SliderIntPresenter |
Slider |
SliderIntModel |
parent.AddSliderInt(view, model) |
SliderIntModel.OnValueChanged, then ValueChanged |
InputFieldPresenter |
TMP_InputField |
string |
parent.AddInputField(view, value[, maxLength]); InputValue reads the field |
ValueChanged |
TextPresenter |
Text |
TextModel |
parent.AddText(view, text) or (view, format, args…) |
— |
TMPPresenter |
TMP_Text |
TextModel |
parent.AddText(view, text) or (view, format, args…) |
— |
HyperlinkTextPresenter |
HyperlinkText |
TextModel |
parent.AddHyperlinkText(view, text) or (view, format, args…) |
— |
ImagePresenter |
Image |
Sprite |
parent.AddImage(view, sprite) |
— |
RawImagePresenter |
RawImage |
Texture (null disables the view) |
parent.AddRawImage(view[, texture]) |
— |
GesturePresenter |
UIGestureDetector |
GestureDelegate |
parent.AddGesture(view, onGesture) |
the model |
ResourcePrefabPresenter |
Transform |
string, a Resources path |
parent.AddResourcePrefab(parentTransform, path) |
— |
Each presenter's helpers are in a static class named after it: ButtonPresenterExtensions,
TogglePresenterExtensions, SliderFloatPresenterExtensions and so on. The rest of the package:
| Type | Namespace | What it is |
|---|---|---|
TextModel |
OpenUGD.Presenters |
A text or key plus optional arguments; Resolve(localization) is the rule the text presenters render by. |
TextModelPresenter<TView> |
OpenUGD.Presenters |
The base of the text presenters: optional localisation and re-rendering on a language change. Implement Render(string). |
ToggleModel, SliderIntModel |
OpenUGD.Presenters |
Immutable models of the toggle and int slider presenters. |
Gesture, GestureDelegate |
OpenUGD.Presenters |
The gesture kinds and the callback of GesturePresenter. |
ILocalization, ILocalizationChanged |
OpenUGD.Presenters |
The optional localisation services the text presenters inject. |
IntervalUpdateExtensions |
OpenUGD.Presenters |
presenter.WithIntervalUpdate(coroutines, scope => model[, interval]) for any presenter with a model, plus a string overload for text presenters; DefaultInterval is 100 ms. |
UnityEventExtensions |
OpenUGD |
unityEvent.Subscribe(lifetime, listener) for UnityEvent to UnityEvent<T0, T1, T2, T3>: adds the listener, removes it when the lifetime ends. |
ButtonExtensions |
OpenUGD |
button.SubscribeOnClick(lifetime, listener) for a button no presenter drives. |
UIGestureDetector |
OpenUGD.UI |
Taps and four-way swipes as ISignals: OnTap, OnSwipeLeft, OnSwipeRight, OnSwipeUp, OnSwipeDown; swipeThresholdOfScreen, detectSwipeOnlyAfterRelease. |
HyperlinkText, HyperlinkClick |
OpenUGD.UI |
Clickable TextMeshPro links: LinkClicked (with a Handled veto), HyperlinkOpenEvent, OpenUrls, Text; FindLinkId and OpenUrl can be overridden. |
Import them from Window > Package Manager > CoreLib uGUI Presenters > Samples. The first three build their UI in
code: add the sample's component to an empty GameObject and enter Play mode. Each folder has a README.md.
| Sample | Shows |
|---|---|
| Settings Screen | A button, a toggle, float and whole-number sliders, labels with format arguments, a TextMeshPro input field and an image, translated by a stub ILocalization, with a language switch that renders every label again through ILocalizationChanged. |
| Recycled List | 500 items, one presenter each, and only as many row views as fit on screen. Scrolling moves row views between presenters with SetView, and the listeners follow the view. |
| Gestures and Links | UIGestureDetector and HyperlinkText with their presenters, a detector subscribed to before its GameObject was ever active, and a LinkClicked handler that takes in-game links and refuses insecure ones. |
| Keyboard Shortcuts | The replacement for the removed AddKeyboard: a helper of about 25 lines you copy into your project, reading the Input System or the Input Manager, bound to a presenter's lifetime. |
The EditMode tests are in the assembly com.openugd.corelib.widgets.tests. Unity compiles a package's tests only
when the package is listed under testables in Packages/manifest.json:
{
"dependencies": {
"com.openugd.corelib.widgets": "2.0.0"
},
"testables": [
"com.openugd.corelib.widgets"
]
}Then open Window > General > Test Runner, choose EditMode and run com.openugd.corelib.widgets.tests. The
project needs the Test Framework package (com.unity.test-framework), which new Unity 6 projects include. The
tests marked [Category("RequiresUnity")] drive real uGUI components, UIGestureDetector and HyperlinkText;
the rest cover the helpers, the signals, the text and localisation rules, the timer and the gesture rules, and also
run on .NET without the editor: level1.sh in openugd/upm-tools builds
them as a .NET test project and leaves out the RequiresUnity ones.
The checks also run in public CI: openugd/upm-tools
compiles this package, its samples and the complete examples in this README (those that declare a type) against
Unity 6000.0's assemblies and runs its engine-free tests on every change there and every Monday. The tests that
need the editor (category RequiresUnity) run in a real Unity 6000.0.41f1 editor before each release.
This section is for users of com.openugd.corelib.widgets 0.5.0; the package ID is unchanged. Version 2.0.0
requires Unity 6000.0 or newer and the 2.0 versions of the OpenUGD family, and is licensed under Apache-2.0 (0.5.0
shipped a modified MIT text). com.openugd.corelib 2.0 changes the presenter base class itself, and
com.openugd.context replaces com.openugd.dependency.injection; their READMEs cover those changes. There are no
[Obsolete] forwarding types: the base class, the lifecycle hooks and the DI layer change together, so old code
does not compile either way.
| What | 0.5.0 | 2.0.0 | What to do |
|---|---|---|---|
| Pattern name | Widget, ButtonWidget, … |
Presenter, ButtonPresenter, … |
Rename; see the table below. |
| Namespace | OpenUGD.Core.Widgets |
OpenUGD.Presenters |
using OpenUGD.Presenters; |
| Change reports | SubscribeOnClick(…) methods; the float slider was an ISignal<float> |
Clicked, Toggled, ValueChanged signal properties |
presenter.Clicked.Subscribe(lifetime, handler) |
| Updates | UpdateText, UpdateRawImage, UpdateSliderInt, AddText on an existing widget |
SetModel |
presenter.SetModel(model) |
| Keyboard | AddKeyboard |
the Keyboard Shortcuts sample | Import the sample; see below. |
| TextMeshPro | your project's com.unity.textmeshpro |
com.unity.ugui 2.0 on Unity 6 |
Nothing to install; keep the Unity.TextMeshPro asmdef reference. |
| Timer | label.WithIntervalUpdate(text) |
label.WithIntervalUpdate(coroutines, text) |
Inject an ICoroutineProvider and pass it. |
| Localisation | OpenUGD.Core in corelib |
OpenUGD.Presenters in this package, optional |
using OpenUGD.Presenters; |
| Event helpers | UnityEngine.UI namespace |
OpenUGD namespace |
using OpenUGD; |
| Gestures | Up and Down swapped; Signal properties |
correct directions; ISignal properties |
Swap back any handlers you swapped to compensate. |
| Renders | SetModel on a slider or an input field raised its change signal or callback |
never reported | Remove code that filtered out your own updates. |
| 0.5.0 | 2.0.0 |
|---|---|
Widget, Widget<TView>, Widget<TView, TModel> |
Presenter, Presenter<TView>, Presenter<TView, TModel> (in com.openugd.corelib) |
WidgetView |
ViewBehaviour (in com.openugd.corelib) |
new Widget.Root(lifetime, injector) |
new Presenter.Root(lifetime, new ContextPresenterFactory(context)) |
parent.AddWidget(widget) |
parent.AddPresenter(presenter) |
ButtonWidget, ToggleWidget, SliderFloatWidget, SliderIntWidget, InputFieldWidget, TextWidget, TMPWidget, HyperlinkTextWidget, ImageWidget, RawImageWidget, GestureWidget, ResourcePrefabWidget |
ButtonPresenter, TogglePresenter, SliderFloatPresenter, SliderIntPresenter, InputFieldPresenter, TextPresenter, TMPPresenter, HyperlinkTextPresenter, ImagePresenter, RawImagePresenter, GesturePresenter, ResourcePrefabPresenter |
ToggleWidgetModel, SliderIntWidgetModel |
ToggleModel, SliderIntModel |
GestureDelegate(GestureWidget sender, …) |
GestureDelegate(GesturePresenter sender, …) |
…WidgetExtensions, InputFieldExtensions, SliderWidgetExtensions, HyperlinkWidgetExtensions |
…PresenterExtensions: InputFieldPresenterExtensions, SliderFloatPresenterExtensions, HyperlinkTextPresenterExtensions, … |
TMPWidgetIntervalUpdateExtensions |
IntervalUpdateExtensions |
parent.AddFloatSlider(view, …) |
parent.AddSliderFloat(view, …) |
parent.AddText(hyperlinkView, format, keys…) |
parent.AddHyperlinkText(hyperlinkView, format, keys…) |
AddInputField(…, maxLenght) |
AddInputField(…, maxLength) |
// 0.5.0
using OpenUGD.Core.Widgets;
var root = new Widget.Root(lifetime, injector);
var sound = root.AddToggle(soundToggle, new ToggleWidgetModel(OnSound, true));
var volume = root.AddFloatSlider(volumeSlider, 0.5f);
// 2.0.0
using OpenUGD.Presenters;
var root = new Presenter.Root(lifetime, new ContextPresenterFactory(context));
var sound = root.AddToggle(soundToggle, new ToggleModel(OnSound, true));
var volume = root.AddSliderFloat(volumeSlider, 0.5f);The hand-written subscribe methods are gone. Each input presenter exposes a subscribe-only ISignal property that
is created on first read and ends with the presenter. SliderIntPresenter gained one; it used to report only
through its model's callback.
// 0.5.0
button.SubscribeOnClick(lifetime, OnAccept);
toggle.SubscribeOnClick(lifetime, OnSound);
input.SubscribeOnValueChanged(lifetime, OnName);
slider.Subscribe(lifetime, OnVolume); // SliderFloatWidget was an ISignal<float>
// 2.0.0
button.Clicked.Subscribe(lifetime, OnAccept);
toggle.Toggled.Subscribe(lifetime, OnSound);
input.ValueChanged.Subscribe(lifetime, OnName);
slider.ValueChanged.Subscribe(lifetime, OnVolume);Affects you if you read a signal before the presenter is attached: that throws InvalidOperationException
now. UIGestureDetector's five signals are ISignal instead of Signal too, so only the detector raises them; a
test that called detector.OnTap.Fire() calls the detector's pointer handlers instead.
// 0.5.0
parent.UpdateText(score, "Score: {0}", 100);
parent.AddText(title, "Paused"); // updated an existing TMPWidget
rawImage.UpdateRawImage(texture);
difficulty.UpdateSliderInt(new SliderIntWidgetModel(0, 2, 1));
// 2.0.0
score.SetModel(new TextModel { Format = "Score: {0}", Keys = new object[] { 100 } });
title.SetModel("Paused");
rawImage.SetModel(texture);
difficulty.SetModel(new SliderIntModel(0, 2, 1));AddKeyboard is removed and nothing in the package replaces it. It was not a presenter, it could fire once more
on the frame after its presenter closed, and it read only UnityEngine.Input, which throws when Active Input
Handling is set to the Input System package alone. Import the Keyboard Shortcuts sample: its
SubscribeOnKeyDown reads the Input System when it is installed and enabled, falls back to the Input Manager, and
never fires after its presenter closes.
// 0.5.0
this.AddKeyboard(KeyCode.LeftArrow, onKeyDown: () => controller.Input.Left());
// 2.0.0, with the sample and [Inject] private ICoroutineProvider _coroutines; on the presenter
this.SubscribeOnKeyDown(_coroutines, KeyCode.LeftArrow, () => controller.Input.Left());The sample's README covers onKey and onKeyUp, which it leaves out, and the keys whose names differ between
KeyCode and the Input System's Key.
On Unity 6, TextMeshPro is part of com.unity.ugui 2.0, and com.unity.textmeshpro is a deprecated 5.0.0 shim.
0.5.0 declared no TextMeshPro dependency and relied on the project installing com.unity.textmeshpro; 2.0.0
declares com.unity.ugui 2.0.0, which contains it, and keeps the TMP presenters in its one runtime assembly, which
references Unity.TextMeshPro. Nothing needs installing; an asmdef of yours keeps its Unity.TextMeshPro
reference, and the TMP Essential Resources are still needed to draw TMP text.
TMPWidgetIntervalUpdateExtensions is IntervalUpdateExtensions. WithIntervalUpdate takes the
ICoroutineProvider as its second parameter, because a presenter no longer exposes its context, and it drives any
presenter with a model, not only a TMP label. It returns the presenter as Presenter<TView, TModel>.
// 0.5.0
TMPWidget timer = label.WithIntervalUpdate(_ => FormatTimeLeft());
// 2.0.0, with [Inject] private ICoroutineProvider _coroutines;
label.WithIntervalUpdate(_coroutines, _ => FormatTimeLeft());It now waits in unscaled time, so a countdown keeps running while Time.timeScale is 0; it allocates one wait per
timer instead of one per tick; and an exception from the first update, which still happens inside the call,
reaches you instead of being logged by the coroutine.
ILocalization and ILocalizationChanged moved from com.openugd.corelib (OpenUGD.Core) to this package
(OpenUGD.Presenters) with their script GUIDs. Both are optional: without them text renders untranslated, and a
format's arguments are still substituted (0.5.0 showed Score: {0} when no localisation was registered).
ILocalizationChanged is now an ISignal, so a Signal that declares it is a complete implementation; an
explicit void ILocalizationChanged.Subscribe(…) becomes void ISignal.Subscribe(…). HyperlinkTextPresenter
localises like the other text presenters: it renders a TextModel, translates the format as well as the
arguments, renders again on a language change, and no longer writes translations into your argument array.
UIGestureDetector reports the direction the pointer moved; 0.5.0 swapped Up and Down. If you swapped your
handlers to compensate, swap them back. detectSwipeOnlyAfterRelease judges the swipe on release instead of
turning swipes off; a press reports one gesture at most, so a swipe is no longer also a tap; only the pointer that
pressed is followed; and the pointer is followed outside the element, which means a ScrollRect above the
detector no longer receives the drags that start on it (a press that a Button inside the area takes is handed on
as before). OnDisable and OnDestroy are protected virtual: a subclass overrides them and calls base.
HyperlinkText raises LinkClicked before opening anything, and a handler can set Handled to refuse the link;
OpenUrls turns opening off. HyperlinkOpenEvent reports only links that were opened. The hit test uses the
press camera, so links line up on camera-space and world-space canvases, and an unassigned Text is filled in
with the label on the same GameObject.
- Renders are silent.
SetModelon a slider or an input field no longer raises its change signal or callback; only the user, or code writing the widget directly, does. - Every
SetModelrenders. 0.5.0's toggle, image and int slider rendered once, when they first had both a model and a view; a laterSetModelleft the toggle and the image as they were, and moved the slider without updating its range. - Listeners follow the view. Replacing, detaching or re-attaching a view moves, removes or re-adds its listener
exactly once. 0.5.0 left the old view's listener in place, and closing after
SetView(null)threw. SliderIntPresenterturns onwholeNumbers.- The hyperlink helpers work without a localisation. 0.5.0's
AddHyperlinkTextthrewNullReferenceExceptionwhen noILocalizationwas registered. - A missing
Resourcesprefab throwsInvalidOperationExceptionnaming the path. UnityEventExtensionsandButtonExtensionsare inOpenUGD(with[MovedFrom]), rejectnullarguments, and do nothing on a terminated lifetime.InputFieldPresenter.InputValuewith no live view throwsInvalidOperationExceptioninstead ofNullReferenceException.
Six packages, versioned together as 2.x and published on OpenUPM
under the com.openugd scope. Installing one brings the ones it depends on.
| Package | What it gives you | Depends on |
|---|---|---|
Lifetime — com.openugd.lifetime |
Scopes with deterministic, reverse-order clean-up | — |
Signal — com.openugd.signal |
Typed events whose subscriptions end with a lifetime | Lifetime |
Context — com.openugd.context |
Dependency injection that validates the whole graph before it builds anything | Lifetime |
CoreLib — com.openugd.corelib |
The Unity boundary: ContextBehaviour, presenters, commands, logging |
Lifetime, Signal, Context |
CoreLib uGUI Presenters — com.openugd.corelib.widgets |
Presenters that bind uGUI and TextMesh Pro controls to a model | CoreLib, Context, Signal, Lifetime, uGUI |
uGUI Components — com.openugd.ui |
Shader-free uGUI components: flip, gradient, invisible hit area | uGUI |
Start with Lifetime and Signal for plain C# scopes and events, add Context for dependency injection, and CoreLib to
run it inside a Unity scene. com.openugd.configuration, a
string-keyed configuration for Context, is 0.x and not on OpenUPM yet. Other com.openugd.* packages on OpenUPM
predate 2.0 and are not part of this family.
The OpenUGD packages share a major version: every package of the family is 2.x. Minor and patch versions move
independently. Each 2.x package works with the 2.x versions of its dependencies at or above the minimums declared
in its package.json.
The changes in each version are listed in CHANGELOG.md.
Report a bug or an idea at
github.com/openugd/upm-corelib-widgets/issues: include
the Unity version, the package version and, for an exception, the full message. To work on the package, clone it,
reference the clone from a Unity 6 project
("com.openugd.corelib.widgets": "file:../path/to/upm-corelib-widgets" in Packages/manifest.json), add
com.openugd.corelib.widgets to testables, and run its tests in the Test Runner. The project also needs
com.openugd.corelib, com.openugd.context, com.openugd.signal and com.openugd.lifetime: keep the scoped
registry from Install, or reference clones of them the same way. The checks CI runs are scripts
in openugd/upm-tools; its README shows how to run them locally.
Apache-2.0. See LICENSE.md.