Skip to content

Commit 5e1cecf

Browse files
authored
feat(library): icon-based row-action buttons on Instantiated tab (#830)
## Summary Replaces the text-labeled Select / Teleport / Remove buttons on each row of the Library's Instantiated tab with icon-only colored beveled squares (80×80). The three buttons preserve their previous semantic hierarchy via the existing `AcceptButton` (green) / `StandardButton` (blue) / `CancelButton` (red) prefab styles. ### User-affecting changes - **Buttons are icon-only.** The existing localization keys (`library.select`, `library.deselect`, `library.teleportTo`, `library.remove`) remain in the locale files for future tooltip / a11y use, so no translations are lost. - **Buttons are hidden (not just disabled) for scene-mode and embedded instances.** Previously these rendered as disabled buttons that still occupied row width and added visual noise. The underlying scene-unload code paths in the OnClicked handlers are intentionally left in place so the functionality can be exposed via a different UI surface later — only the library-window entry point goes away. ### Sprite additions (`com.basis.sdk`) | Sprite | Source | Status | |---|---|---| | `scan-outline.png` | Ionicons | new | | `link-outline.png` | Ionicons | new, consumed by follow-up trackerobjects integration PR | | `unlink-outline.png` | Ionicons | new, consumed by follow-up trackerobjects integration PR | | `trash-bin-outline.png` | already on disk | newly registered as addressable in the Basis UI Assets group | All sprites match the existing project icon convention: white-stroke RGBA at 512×512 on transparent background. Source: https://github.com/ionic-team/ionicons (MIT). PNGs rendered from the upstream SVGs. ### API additions Five new entries on `AddressableAssets.Sprites`: `Select`, `TeleportTo`, `Trash`, `Link`, `Unlink`. `TeleportTo` aliases the existing `Teleport.png` (was only exposed via `Respawn` before). `Link` and `Unlink` ship here for the follow-up trackerobjects integration package PR to consume. ## Required checks All boxes below must be ticked before this PR can merge. If a check is genuinely N/A, tick it anyway and explain under **Notes**. <!-- required-checks-start --> <!-- Tick the boxes in place — do not edit the line text. The pr-checklist workflow parses this block; per-PR context goes under Notes. --> - [x] **Tested** — I built and ran this locally. The change works in the editor and (where relevant) in a built player. - [x] **Transform access is combined and limited** — In hot paths, transform reads/writes go through `TransformAccessArray` or are otherwise batched. I have not added per-frame `transform.position` / `transform.rotation` / `transform.localPosition` calls inside loops. Whenever I need both position and rotation, I use the combined APIs — `SetPositionAndRotation` / `SetLocalPositionAndRotation` for writes, `GetPositionAndRotation` / `GetLocalPositionAndRotation` for reads — instead of two separate property accesses; the combined call does one local-to-world matrix traversal instead of two. - [x] **Addressables used for asset/memory loading** — Any new asset loads go through Addressables. No new `Resources.Load`, no direct asset references that pull large content into memory on scene load. - [x] **No new `GetComponent` / `AddComponent` where avoidable** — Where unavoidable, the result is cached on a field, and any `GetComponent<T>` is replaced with `TryGetComponent<T>(out var x)` — bare `GetComponent` will be denied. `TryGetComponent` is the modern API (Unity 2019.2+) and skips the Editor-only GC allocation `GetComponent` causes when a component is missing: Unity wraps the `null` return in a managed "fake null" object so its overloaded `==` operator can still detect destroyed C++ objects, and constructing that wrapper allocates; `TryGetComponent` returns a `bool` plus `out` parameter and never builds the wrapper. None of these calls run inside `Update`, `LateUpdate`, `FixedUpdate`, jobs, or other per-frame code paths. - [x] **Per-frame work is scheduled through `BasisEventDriver`** — Any new per-frame work hooks into `BasisEventDriver` rather than adding standalone `Update` / `LateUpdate` / `FixedUpdate` callbacks on a MonoBehaviour. - [x] **Considered jobification** — I asked whether this work can be moved to a Unity Job (Burst-compiled where possible). If it can, it is. If it cannot, the reason is in **Notes**. - [x] **No needless `{ get; set; }` properties or access lockdowns** — Public fields are fine; Basis is meant to be read and modified freely, so don't wall things off `private`/`internal` without a real reason. Don't wrap a field in `{ get; set; }` when the accessors do nothing — property accessors have a real performance cost vs direct field access, and the lead maintainer prefers plain fields (or a method / setter-only property when only the setter needs logic) over a noop-getter pair. For `.Instance` singletons, callers reassigning `Type.Instance` is allowed; if that would break your code, log a warning or throw — don't block the assignment. Locking down access is not your call. - [x] **Camera access goes through `BasisLocalCameraDriver`** — Code that needs the local camera (transform, projection, rig data, etc.) pulls it from `BasisLocalCameraDriver` rather than looking one up itself. Don't roll a separate camera discovery path. - [x] **Logging uses `BasisDebug`** — All new logging calls go through `BasisDebug.Log` / `BasisDebug.LogWarning` / `BasisDebug.LogError` (with an appropriate `LogTag`) instead of `UnityEngine.Debug.Log` / `Debug.LogWarning` / `Debug.LogError`. `BasisDebug` routes through Basis's tagged, color-coded logger and respects the project-wide `LoggingDisabled` toggle so logging can be killed at runtime; bare `Debug.Log` calls bypass that and will be denied. - [x] **No scene-wide discovery for dependencies** — New code is architected so it does not need `FindObjectOfType` / `FindObjectsOfType` / `GameObject.Find` / `FindGameObjectsWithTag` to locate what it depends on. References are wired in — registered through an existing manager/driver, injected at init, or passed in by the caller — rather than discovered by scanning the scene at runtime. If a scene scan is genuinely unavoidable, justify it under **Notes**. - [x] **No allocations in hot paths** — Per-frame code (Update / LateUpdate / FixedUpdate, simulation loops, jobs, anything called once per frame or more) does not allocate. No `new` on reference types, no LINQ, no `string` concatenation/interpolation, no boxing, no `foreach` over interface-typed collections. Allocate once at init and reuse the buffer. - [x] **No debugging in hot paths** — No log calls of any kind on per-frame paths, including `BasisDebug`. Hot-path logging floods the console and incurs cost on every frame regardless of whether the message is filtered out downstream. If a hot-path log is needed while iterating, gate it behind `#if UNITY_EDITOR` and remove (or leave gated) before merge. - [x] **Hot-path collection access is optimized** — Cache `.Count` (lists) / `.Length` (arrays) into a local `int` before the loop instead of re-reading the property each iteration. Prefer `T[]` (with a separate length int when the array is over-sized) over `List<T>` where the data is hot — Unity's mono BCL doesn't expose `CollectionsMarshal.AsSpan(List<T>)`, so a list can't be fed into `Span<T>` / unsafe paths cleanly. Where the perf justifies it, drop into `Span<T>` / `ref` locals / `Unsafe.As` / `unsafe` pointer code to skip bounds checks and copies, and call out the invariants you're relying on under **Notes** so reviewers can sanity-check them. <!-- required-checks-end --> ## Testing details Tick the platforms you actually tested on. Leave the rest unticked — these are informational and do not block merge. - [x] Windows - [ ] Linux - [ ] Android - [ ] iOS - [ ] macOS Input / control mode coverage: - [ ] Tested in VR (note headset under **Notes**) - [x] Tested in desktop / non-VR mode - [ ] Tested with phone controls (mobile touch input) - [ ] N/A — change does not touch player/XR/input code Where applicable, confirm these flows still work after your changes: - [ ] Hot-switching (desktop ↔ VR mode swap at runtime) - [ ] Avatar swapping - [ ] Server swapping (joining / leaving / changing servers) - [x] N/A — change does not touch any of the above ## Notes - **Required-check N/A justifications.** This is a UI-only change: row-button restyling plus a few new addressable sprite entries. No transform access, no per-frame work, no jobification candidates, no camera access, no new logging, no scene-wide discovery, no hot-path code, no `GetComponent`/`AddComponent` additions, no `{ get; set; }` lockdowns. Existing `TryGetComponent<Button>` calls in `LibraryProvider.CreateListEntry` are retained unchanged. The required-checks boxes are ticked because the project's checklist rule is to tick + explain N/A items here; they aren't claims that the diff exercised those code paths. - **Verified live in the editor.** Buttons were iterated visually across multiple in-game reviews — caption crowding, icon stroke colour (white to match the existing Ionicon set), and the colored-bevel vs Hotbar layout were all tightened based on what rendered correctly on a real avatar + prop spawn. - **Scene/embedded unload code paths intact.** The `OnClicked` handlers for Teleport and Remove still contain their original `case SpawnMode.Scene` branches and the `case SpawnMethod.Embedded` fall-through label — the early return at the top of `CreateListEntry` simply prevents this UI surface from ever reaching them. Those code paths predate this PR and may want to be surfaced via a different UI (a moderator panel, perhaps) in a follow-up; deliberately not removed here. - **Follow-up trackerobjects PR** depends on this one merging first so that `AddressableAssets.Sprites.Link` / `.Unlink` resolve at compile time. Its branch is `feat/trackerobjects` on this fork; the icon-swap commit on that branch is held back until this lands.
2 parents c55b8d6 + a395153 commit 5e1cecf

9 files changed

Lines changed: 432 additions & 18 deletions

File tree

Basis/Assets/AddressableAssetsData/AssetGroups/Basis UI Assets.asset

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ MonoBehaviour:
116116
m_ReadOnly: 0
117117
m_SerializedLabels: []
118118
FlaggedDuringContentUpdateRestriction: 0
119+
- m_GUID: 2b9d4f1a7e8c3b6d5a2f9e1c4b7a8d3f
120+
m_Address: Packages/com.basis.sdk/Textures/Runtime/unlink-outline.png
121+
m_ReadOnly: 0
122+
m_SerializedLabels: []
123+
FlaggedDuringContentUpdateRestriction: 0
119124
- m_GUID: 2f02813bc29de864cb7a5a27364ee32a
120125
m_Address: Packages/com.basis.sdk/Prefabs/Panel Elements/PE Image Simple Square.prefab
121126
m_ReadOnly: 0
@@ -179,6 +184,11 @@ MonoBehaviour:
179184
m_ReadOnly: 0
180185
m_SerializedLabels: []
181186
FlaggedDuringContentUpdateRestriction: 0
187+
- m_GUID: 4e8a2c9b1d3f6e7a8b9c0d1e2f3a4b5c
188+
m_Address: Packages/com.basis.sdk/Textures/Runtime/scan-outline.png
189+
m_ReadOnly: 0
190+
m_SerializedLabels: []
191+
FlaggedDuringContentUpdateRestriction: 0
182192
- m_GUID: 50f97b6b5d2b5cd4993f1fd38883bde8
183193
m_Address: Packages/com.basis.sdk/Sprites/Icons/Exit.png
184194
m_ReadOnly: 0
@@ -248,6 +258,11 @@ MonoBehaviour:
248258
m_ReadOnly: 0
249259
m_SerializedLabels: []
250260
FlaggedDuringContentUpdateRestriction: 0
261+
- m_GUID: 75dcae065f0db18479883c0f554885bf
262+
m_Address: Packages/com.basis.sdk/Textures/Runtime/trash-bin-outline.png
263+
m_ReadOnly: 0
264+
m_SerializedLabels: []
265+
FlaggedDuringContentUpdateRestriction: 0
251266
- m_GUID: 7789be676b03fdd4f8d2e4b183e25858
252267
m_Address: Packages/com.basis.sdk/Textures/Runtime/clock.png
253268
m_ReadOnly: 0
@@ -263,6 +278,11 @@ MonoBehaviour:
263278
m_ReadOnly: 0
264279
m_SerializedLabels: []
265280
FlaggedDuringContentUpdateRestriction: 0
281+
- m_GUID: 7f3a1d8b4c6e9f2a5b8c3d1e6f4a9b2c
282+
m_Address: Packages/com.basis.sdk/Textures/Runtime/link-outline.png
283+
m_ReadOnly: 0
284+
m_SerializedLabels: []
285+
FlaggedDuringContentUpdateRestriction: 0
266286
- m_GUID: 7f82d6af13c883d4b885adbeab63a58d
267287
m_Address: Packages/com.basis.sdk/Textures/Runtime/exit-outline.png
268288
m_ReadOnly: 0

Basis/Packages/com.basis.framework/BasisUI/Addressables/AddressableAsset.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ public static class Sprites
4040
public static string MicrophoneMute = "Packages/com.basis.sdk/Textures/Runtime/microphone-mute-solid.png";
4141
public static string People = "Packages/com.basis.sdk/Textures/Runtime/people-outline.png";
4242

43+
// row-action icons (Library Instantiated tab)
44+
public static string Select = "Packages/com.basis.sdk/Textures/Runtime/scan-outline.png";
45+
public static string TeleportTo = "Packages/com.basis.sdk/Textures/Runtime/Teleport.png";
46+
public static string Trash = "Packages/com.basis.sdk/Textures/Runtime/trash-bin-outline.png";
47+
public static string Link = "Packages/com.basis.sdk/Textures/Runtime/link-outline.png";
48+
public static string Unlink = "Packages/com.basis.sdk/Textures/Runtime/unlink-outline.png";
49+
4350
// embedded items
4451
public static string Embedded = "Packages/com.basis.sdk/Textures/Runtime/embedded.png";
4552

Basis/Packages/com.basis.framework/BasisUI/Menus/Library/LibraryProvider.cs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,17 +1944,18 @@ private static void CreateListEntry(BasisRuntimeSpawnRegistry.SpawnInstance item
19441944
itemTextInfo.Descriptor.SetHeight(50);
19451945
itemTextInfo.Descriptor.SetWidth(400);
19461946

1947-
PanelButton selectItem = PanelButton.CreateNew(ButtonStyles.AcceptButton, itemListPanel.TabButtonParent);
1948-
selectItem.Descriptor.SetTitle(hasSelected ? BasisLocalization.Get("library.deselect") : BasisLocalization.Get("library.select"));
1949-
selectItem.SetSize(new Vector2(200, 60));
1950-
1951-
// determine if we can select this item
1952-
if (selectItem.Descriptor.gameObject.TryGetComponent<Button>(out Button selectButtonComponent))
1947+
// Skip the row-action buttons for scene-mode and embedded rows — not user-owned spawns.
1948+
if (itemKey.SpawnMode == BasisRuntimeSpawnRegistry.SpawnMode.Scene
1949+
|| itemKey.SpawnMethod == BasisRuntimeSpawnRegistry.SpawnMethod.Embedded)
19531950
{
1954-
// for the moment disable selecting embedded items
1955-
selectButtonComponent.interactable = (itemKey.SpawnMode != BasisRuntimeSpawnRegistry.SpawnMode.Scene) && !(itemKey.SpawnMethod == BasisRuntimeSpawnRegistry.SpawnMethod.Embedded);
1951+
return;
19561952
}
19571953

1954+
PanelButton selectItem = PanelButton.CreateNew(ButtonStyles.AcceptButton, itemListPanel.TabButtonParent);
1955+
selectItem.Descriptor.SetTitle(string.Empty);
1956+
selectItem.SetIcon(AddressableAssets.Sprites.Select);
1957+
selectItem.SetSize(new Vector2(80, 80));
1958+
19581959
selectItem.OnClicked += async () =>
19591960
{
19601961
if(hasSelected)
@@ -1975,14 +1976,9 @@ private static void CreateListEntry(BasisRuntimeSpawnRegistry.SpawnInstance item
19751976
};
19761977

19771978
PanelButton TeleportToItem = PanelButton.CreateNew(ButtonStyles.StandardButton, itemListPanel.TabButtonParent);
1978-
TeleportToItem.Descriptor.SetTitle(BasisLocalization.Get("library.teleportTo"));
1979-
TeleportToItem.SetSize(new Vector2(200, 60));
1980-
// dont let the teleport button work for scenes yet
1981-
if (TeleportToItem.Descriptor.gameObject.TryGetComponent<Button>(out Button teleportButtonComponent))
1982-
{
1983-
// if the item is embedded only allow an admin to interact
1984-
teleportButtonComponent.interactable = !(itemKey.SpawnMode == BasisRuntimeSpawnRegistry.SpawnMode.Scene);
1985-
}
1979+
TeleportToItem.Descriptor.SetTitle(string.Empty);
1980+
TeleportToItem.SetIcon(AddressableAssets.Sprites.TeleportTo);
1981+
TeleportToItem.SetSize(new Vector2(80, 80));
19861982

19871983
TeleportToItem.OnClicked += () =>
19881984
{
@@ -2013,8 +2009,9 @@ private static void CreateListEntry(BasisRuntimeSpawnRegistry.SpawnInstance item
20132009
};
20142010

20152011
PanelButton removeItem = PanelButton.CreateNew(ButtonStyles.CancelButton, itemListPanel.TabButtonParent);
2016-
removeItem.Descriptor.SetTitle(BasisLocalization.Get("library.remove"));
2017-
removeItem.SetSize(new Vector2(200, 60));
2012+
removeItem.Descriptor.SetTitle(string.Empty);
2013+
removeItem.SetIcon(AddressableAssets.Sprites.Trash);
2014+
removeItem.SetSize(new Vector2(80, 80));
20182015

20192016
// only apply this to items that are spawned on the network
20202017
if(itemKey.SpawnMethod == BasisRuntimeSpawnRegistry.SpawnMethod.Network)
8.92 KB
Loading

Basis/Packages/com.basis.sdk/Textures/Runtime/link-outline.png.meta

Lines changed: 130 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
6.62 KB
Loading

Basis/Packages/com.basis.sdk/Textures/Runtime/scan-outline.png.meta

Lines changed: 130 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
8.66 KB
Loading

0 commit comments

Comments
 (0)