ovui-widgets-app 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. ovui_widgets/app/__init__.py +53 -0
  2. ovui_widgets/app/__main__.py +83 -0
  3. ovui_widgets/app/_input_bridge.py +288 -0
  4. ovui_widgets/app/_input_drain.py +232 -0
  5. ovui_widgets/app/_input_keymap.py +209 -0
  6. ovui_widgets/app/_message_dispatcher.py +307 -0
  7. ovui_widgets/app/application.py +4760 -0
  8. ovui_widgets/app/cli_settings.py +102 -0
  9. ovui_widgets/app/component_loader.py +74 -0
  10. ovui_widgets/app/components.py +151 -0
  11. ovui_widgets/app/create_menu.py +243 -0
  12. ovui_widgets/app/frame_clock.py +160 -0
  13. ovui_widgets/app/headless.py +89 -0
  14. ovui_widgets/app/inspector_state.py +3374 -0
  15. ovui_widgets/app/layout.py +466 -0
  16. ovui_widgets/app/menu_bar.py +1619 -0
  17. ovui_widgets/app/menu_hooks.py +585 -0
  18. ovui_widgets/app/native_runtime_bootstrap.py +121 -0
  19. ovui_widgets/app/settings_dialog.py +143 -0
  20. ovui_widgets/app/status_bar.py +80 -0
  21. ovui_widgets/app/style/__init__.py +73 -0
  22. ovui_widgets/app/style/constants.py +84 -0
  23. ovui_widgets/app/style/imgui_runtime.py +246 -0
  24. ovui_widgets/app/style/styles.py +788 -0
  25. ovui_widgets/app/testing/__init__.py +31 -0
  26. ovui_widgets/app/testing/mock_backend.py +35 -0
  27. ovui_widgets/app/widget_registry.py +169 -0
  28. ovui_widgets/app/window_hooks.py +279 -0
  29. ovui_widgets_agent_skills/__init__.py +50 -0
  30. ovui_widgets_agent_skills/skills/omniverse-ui-styling/SKILL.md +91 -0
  31. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/centralized-style-module.md +118 -0
  32. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/developer-guide.md +152 -0
  33. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/global-styles-and-startup.md +200 -0
  34. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/naming-constants.md +119 -0
  35. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/naming-selectors.md +71 -0
  36. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/style-hierarchy.md +193 -0
  37. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/style-mechanics.md +179 -0
  38. ovui_widgets_agent_skills/skills/omniverse-ui-styling/references/target-architecture.md +65 -0
  39. ovui_widgets_agent_skills/skills/omniverse-ui-widgets/SKILL.md +160 -0
  40. ovui_widgets_agent_skills/skills/omniverse-ui-widgets/references/app-skeleton.md +239 -0
  41. ovui_widgets_agent_skills/skills/omniverse-ui-widgets/references/menu-and-qa.md +139 -0
  42. ovui_widgets_agent_skills/skills/omniverse-ui-widgets/references/runtime-environment.md +96 -0
  43. ovui_widgets_agent_skills/skills/omniverse-ui-widgets/references/source-map.md +67 -0
  44. ovui_widgets_agent_skills/skills/omniverse-ui-widgets/references/usd-and-dataflow.md +262 -0
  45. ovui_widgets_agent_skills/skills/omniverse-ui-widgets-app/SKILL.md +119 -0
  46. ovui_widgets_app-0.2.0.dist-info/METADATA +89 -0
  47. ovui_widgets_app-0.2.0.dist-info/RECORD +50 -0
  48. ovui_widgets_app-0.2.0.dist-info/WHEEL +5 -0
  49. ovui_widgets_app-0.2.0.dist-info/entry_points.txt +11 -0
  50. ovui_widgets_app-0.2.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,53 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """OvGear: Kit-free 3D application built on standalone omni.ui.
12
+
13
+ Top-level imports are lazy so that ``import ovui_widgets.app`` does not pull in
14
+ ``omni.ui`` (or anything that imports it). This is required by the
15
+ headless entrypoint (``python -m ovui_widgets.app.headless``), which must set
16
+ ``OMNIUI_HEADLESS=1`` and ``OMNIUI_BACKEND=vulkan`` *before* the ovui
17
+ platform is initialised. Eagerly importing :class:`Application` here
18
+ would defeat that ordering, because Python imports the package
19
+ ``ovui_widgets.app`` (and runs this file) before executing ``headless.py``.
20
+ """
21
+
22
+ from ovui_widgets.common.version import package_version as _package_version
23
+
24
+ __version__ = _package_version("ovui-widgets-app")
25
+
26
+ __all__ = [
27
+ "AppWindowContribution",
28
+ "AppWindowHandle",
29
+ "AppWindowRegistry",
30
+ "Application",
31
+ "ContentBrowserWindow",
32
+ "ErrorReporter",
33
+ "StatusBar",
34
+ ]
35
+
36
+
37
+ def __getattr__(name):
38
+ if name == "Application":
39
+ from ovui_widgets.app.application import Application
40
+ return Application
41
+ if name in {"AppWindowContribution", "AppWindowHandle", "AppWindowRegistry"}:
42
+ from ovui_widgets.app import window_hooks
43
+ return getattr(window_hooks, name)
44
+ if name == "ContentBrowserWindow":
45
+ from ovui_widgets.content import ContentBrowserWindow
46
+ return ContentBrowserWindow
47
+ if name == "ErrorReporter":
48
+ from ovui_widgets.common.error_reporter import ErrorReporter
49
+ return ErrorReporter
50
+ if name == "StatusBar":
51
+ from ovui_widgets.app.status_bar import StatusBar
52
+ return StatusBar
53
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,83 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ import argparse
12
+ import sys
13
+ from typing import List, Optional
14
+
15
+
16
+ async def _main_async() -> None:
17
+ """Cooperative entry point for embedding OvGear in an existing asyncio loop.
18
+
19
+ Callers are responsible for the synchronous bootstrap (write_split_ini,
20
+ ui.init, style setup) before awaiting this coroutine. For a standalone
21
+ launch use :func:`main`, which delegates to :meth:`Application.run`.
22
+ """
23
+ from ovui_widgets.app.application import Application
24
+
25
+ app = Application()
26
+ app._running = True
27
+ try:
28
+ await app.run_async()
29
+ finally:
30
+ app._running = False
31
+
32
+
33
+ def _parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
34
+ from ovui_widgets.app.cli_settings import extract_setting_overrides
35
+
36
+ parser = argparse.ArgumentParser(
37
+ description="OvGear — standalone 3D USD viewer",
38
+ epilog=(
39
+ "Settings overrides: any --/path/to/key=value argument sets the "
40
+ "corresponding dotted setting at launch (e.g. --/ui/theme=light "
41
+ "sets ui.theme). Overrides take precedence over persisted settings."
42
+ ),
43
+ )
44
+ parser.add_argument(
45
+ "usd_file",
46
+ nargs="?",
47
+ default=None,
48
+ help="Optional path to a USD file (.usd/.usda/.usdc/.usdz) to open on startup.",
49
+ )
50
+ if argv is None:
51
+ argv = sys.argv[1:]
52
+ try:
53
+ overrides, remaining = extract_setting_overrides(list(argv))
54
+ except ValueError as exc:
55
+ parser.error(str(exc))
56
+ args = parser.parse_args(remaining)
57
+ args.settings_overrides = overrides
58
+ return args
59
+
60
+
61
+ def main(argv: Optional[List[str]] = None) -> None:
62
+ """Launch OvGear. Used by both `python -m ovui_widgets.app` and the `ovui-widgets` console script."""
63
+ args = _parse_args(argv if argv is not None else sys.argv[1:])
64
+ from ovui_widgets.app.native_runtime_bootstrap import (
65
+ install_preconstructed_renderer,
66
+ preconstruct_selected_native_renderer,
67
+ )
68
+
69
+ bootstrap = preconstruct_selected_native_renderer()
70
+ from ovui_widgets.app.application import Application
71
+
72
+ app = Application(settings_overrides=args.settings_overrides)
73
+ install_preconstructed_renderer(app, bootstrap)
74
+ app.run(usd_path=args.usd_file)
75
+
76
+
77
+ def main_sync() -> None:
78
+ """Console script entry point declared in pyproject.toml. Delegates to main()."""
79
+ main()
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
@@ -0,0 +1,288 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """Tier 3 livestream input bridge — SDK callback ↔ ovgear main loop.
12
+
13
+ The ovstream SDK delivers input events on a worker thread; ovgear's
14
+ ImGui IO must be written on the main loop thread before
15
+ ``ui.next_frame()``. ``RemoteInputBridge`` is the lock-protected
16
+ hand-off:
17
+
18
+ * ``MouseEventType.MOVE`` events coalesce into a single atomic
19
+ ``(x, y)`` — only the most recent cursor position survives until
20
+ the next drain.
21
+ * Button presses, wheel ticks, keyboard events and unicode text
22
+ strings append to a deque.
23
+ * Consecutive wheel events with the same modifier mask accumulate
24
+ their deltas, so a fast scroll spinning out 30 ticks in one frame
25
+ collapses to a single injected scroll.
26
+
27
+ Coordinates arrive in streamed-frame pixel space — no scaling. The SDK
28
+ clamps inclusively to its announced extents
29
+ (``input_handler.cpp:119–120``), so ``x == width`` is reachable. The
30
+ bridge accepts that range and clamps it to the ovui-indexable range
31
+ ``[0, width-1] × [0, height-1]``. The first out-of-range coord raises
32
+ a one-time WARN; subsequent out-of-range traffic is silent so a
33
+ mis-sized client cannot flood the log.
34
+
35
+ This module is the **skeleton** (issue #34, Step 3.2). The pre-tick
36
+ drain loop (Step 3.3), mouse-button enum mapping (Step 3.4), modifier
37
+ disconnect cleanup (Step 3.5), and ``Server`` callback wiring (Step
38
+ 3.6) layer on top of this state object.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import logging
44
+ import threading
45
+ from collections import deque
46
+ from typing import Deque, List, Tuple, Union
47
+
48
+ from ovstream import KeyboardEvent, KeyState, MouseEvent, MouseEventType
49
+
50
+ _LOG = logging.getLogger("ovui_widgets.app.input_bridge")
51
+
52
+ # NVST key codes that count as "modifier keys" for disconnect cleanup
53
+ # (KeyDefs.h:138–155). Caps Lock and Num Lock are deliberately omitted:
54
+ # they are toggle keys, not held modifiers, so synthesising a release
55
+ # on disconnect would falsely tell ovui the user just released CapsLock
56
+ # even when they are still toggling it.
57
+ #
58
+ # Tracking the actual key codes (not an aggregate ``modifiers`` bitmask)
59
+ # is the source-of-truth fix for the Codex Step 3.5 NOT-GOOD finding:
60
+ # the runtime ovstream WebRTC handler at
61
+ # ``kit-livestream/sdk/src/webrtc/input_handler.cpp:205–224`` collapses
62
+ # RSHIFT / RCONTROL / RALT into the *generic* ``kModShift / kModControl
63
+ # / kModAlt`` bits and reuses bits ``0x0010`` and ``0x0020`` for
64
+ # CapsLock / NumLock. The aggregate ``modifiers`` mask therefore tells
65
+ # us *something* is held but not which side; only the per-event
66
+ # ``key_code`` is side-aware.
67
+ _MODIFIER_NVST_KEY_CODES = frozenset({
68
+ 0x0301, # NVST_KEY_SHIFT
69
+ 0x0302, # NVST_KEY_LSHIFT
70
+ 0x0303, # NVST_KEY_RSHIFT
71
+ 0x0304, # NVST_KEY_CONTROL
72
+ 0x0305, # NVST_KEY_LCONTROL
73
+ 0x0306, # NVST_KEY_RCONTROL
74
+ 0x0307, # NVST_KEY_ALT
75
+ 0x0308, # NVST_KEY_LALT
76
+ 0x0309, # NVST_KEY_RALT
77
+ 0x0310, # NVST_KEY_META
78
+ 0x0311, # NVST_KEY_LMETA
79
+ 0x0312, # NVST_KEY_RMETA
80
+ })
81
+
82
+ # Discrete events the bridge accepts in its deque. KeyboardEvent and
83
+ # MouseEvent are stored verbatim; unicode-text callbacks land as plain
84
+ # ``str`` so the drain can dispatch with ``isinstance``.
85
+ BridgeEvent = Union[KeyboardEvent, MouseEvent, str]
86
+
87
+
88
+ class RemoteInputBridge:
89
+ """Lock-protected SDK→main-loop event buffer."""
90
+
91
+ def __init__(self, width: int, height: int) -> None:
92
+ if width <= 0 or height <= 0:
93
+ raise ValueError(
94
+ f"extents must be positive, got width={width} height={height}"
95
+ )
96
+ self._lock = threading.Lock()
97
+ self._width: int = int(width)
98
+ self._height: int = int(height)
99
+ self._mouse_xy: Tuple[int, int] = (0, 0)
100
+ self._mouse_modifier_mask: int = 0
101
+ self._mouse_modifier_dirty: bool = False
102
+ self._events: Deque[BridgeEvent] = deque()
103
+ # Press-ordered list of NVST modifier key codes that the SDK
104
+ # delivered DOWN events for and has not yet delivered matching
105
+ # UP events for. The disconnect-cleanup path reads this so the
106
+ # synthesised releases use the **same** key code (and therefore
107
+ # the same ImGui side) the client originally pressed.
108
+ self._held_modifier_keys: List[int] = []
109
+ # Cached aggregate ``modifiers`` mask from the last keyboard
110
+ # event. Kept for diagnostics only — Codex's Step 3.5 review
111
+ # established that this mask is not side-aware on the runtime
112
+ # ovstream WebRTC path, so cleanup reads the held-keys list
113
+ # above instead.
114
+ self._last_modifier_mask: int = 0
115
+ self._out_of_range_warned: bool = False
116
+
117
+ # ------------------------------------------------------------------
118
+ # Configuration
119
+ # ------------------------------------------------------------------
120
+
121
+ def set_extents(self, width: int, height: int) -> None:
122
+ """Update the clamp window (e.g. on resolution change)."""
123
+ if width <= 0 or height <= 0:
124
+ return
125
+ with self._lock:
126
+ self._width = int(width)
127
+ self._height = int(height)
128
+
129
+ # ------------------------------------------------------------------
130
+ # SDK-side callbacks (run on the ovstream worker thread)
131
+ # ------------------------------------------------------------------
132
+
133
+ def on_mouse_event(self, event: MouseEvent) -> None:
134
+ mouse_modifier_mask = int(event.modifiers)
135
+ if event.type == MouseEventType.BUTTON and event.button_state == KeyState.UP:
136
+ mouse_modifier_mask = 0
137
+ if event.type == MouseEventType.MOVE:
138
+ x, y = self._clamp(event.x, event.y)
139
+ with self._lock:
140
+ self._mouse_xy = (x, y)
141
+ self._set_mouse_modifier_mask_unlocked(mouse_modifier_mask)
142
+ return
143
+
144
+ if event.type == MouseEventType.WHEEL:
145
+ with self._lock:
146
+ self._set_mouse_modifier_mask_unlocked(mouse_modifier_mask)
147
+ tail = self._events[-1] if self._events else None
148
+ if (
149
+ isinstance(tail, MouseEvent)
150
+ and tail.type == MouseEventType.WHEEL
151
+ and tail.modifiers == event.modifiers
152
+ ):
153
+ tail.data += event.data
154
+ tail.data2 += event.data2
155
+ tail.scroll_x += event.scroll_x
156
+ tail.scroll_y += event.scroll_y
157
+ return
158
+ self._events.append(event)
159
+ return
160
+
161
+ # MouseEventType.BUTTON
162
+ with self._lock:
163
+ self._set_mouse_modifier_mask_unlocked(mouse_modifier_mask)
164
+ self._events.append(event)
165
+
166
+ def on_keyboard_event(self, event: KeyboardEvent) -> None:
167
+ # Steady-state: the SDK delivers each modifier press/release as
168
+ # an ordinary KeyboardEvent (``InputHandler.cpp:230–249``), so
169
+ # the bridge appends it verbatim. The held-keys list and the
170
+ # diagnostic mask are tracked **only** to support disconnect
171
+ # cleanup — no diff-and-emit synthesis in the steady-state
172
+ # path (Codex finding 10b).
173
+ key_code = int(event.key_code)
174
+ with self._lock:
175
+ self._events.append(event)
176
+ self._last_modifier_mask = int(event.modifiers)
177
+ if key_code in _MODIFIER_NVST_KEY_CODES:
178
+ if event.key_state == KeyState.DOWN:
179
+ if key_code not in self._held_modifier_keys:
180
+ self._held_modifier_keys.append(key_code)
181
+ else:
182
+ # KeyState.UP — drop the key from the held list if
183
+ # present. ``list.remove`` raises ValueError if the
184
+ # item is missing; guard so a stray UP without a
185
+ # matching DOWN does not crash the worker thread.
186
+ try:
187
+ self._held_modifier_keys.remove(key_code)
188
+ except ValueError:
189
+ pass
190
+
191
+ def on_unicode(self, text: str) -> None:
192
+ if not text:
193
+ return
194
+ with self._lock:
195
+ self._events.append(text)
196
+
197
+ def on_connection(self, connected: bool) -> None:
198
+ """Handle a connection state change from the ovstream ``Server``.
199
+
200
+ On disconnect, every modifier the SDK delivered a DOWN event
201
+ for (and has not yet delivered a matching UP event for) is
202
+ converted into a synthetic ``KeyState.UP``
203
+ :class:`KeyboardEvent` and appended to the deque. The drain
204
+ dispatches those releases through the same
205
+ :func:`_input_drain.drain_bridge_into_ui` path as ordinary
206
+ keyboard events, so ovui sees a release for every modifier the
207
+ remote client was holding when the link dropped — preventing a
208
+ sticky Shift / Ctrl / Alt / Super state.
209
+
210
+ The synthesised release uses the **same NVST key code** the
211
+ client pressed, so right-side modifiers release right-side
212
+ ImGui keys (Codex Step 3.5 NOT-GOOD fix). Aggregate
213
+ ``modifiers`` mask bits are not consulted because the runtime
214
+ ovstream WebRTC path collapses RSHIFT/RCONTROL/RALT into the
215
+ generic Shift/Control/Alt bits and reuses bits 0x0010/0x0020
216
+ for CapsLock/NumLock.
217
+
218
+ Idempotent: clearing the held list at the end means a second
219
+ ``on_connection(False)`` finds it empty and is a no-op.
220
+ """
221
+ if connected:
222
+ return
223
+ with self._lock:
224
+ for nvst_key in self._held_modifier_keys:
225
+ self._events.append(
226
+ KeyboardEvent(
227
+ key_code=nvst_key,
228
+ scan_code=0,
229
+ modifiers=0,
230
+ key_state=KeyState.UP,
231
+ )
232
+ )
233
+ self._held_modifier_keys.clear()
234
+ self._last_modifier_mask = 0
235
+
236
+ # ------------------------------------------------------------------
237
+ # Main-loop side
238
+ # ------------------------------------------------------------------
239
+
240
+ def drain(self) -> Tuple[Tuple[int, int], List[BridgeEvent]]:
241
+ """Snapshot the current cursor + queued events, clearing the deque."""
242
+ with self._lock:
243
+ xy = self._mouse_xy
244
+ events = list(self._events)
245
+ self._events.clear()
246
+ return xy, events
247
+
248
+ def mouse_modifier_mask(self) -> int:
249
+ """Return the latest mouse-event modifier mask."""
250
+ with self._lock:
251
+ return self._mouse_modifier_mask
252
+
253
+ def consume_mouse_modifier_state(self) -> Tuple[int, bool]:
254
+ """Return ``(mask, changed)`` and clear the changed flag."""
255
+ with self._lock:
256
+ mask = self._mouse_modifier_mask
257
+ changed = self._mouse_modifier_dirty
258
+ self._mouse_modifier_dirty = False
259
+ return mask, changed
260
+
261
+ # ------------------------------------------------------------------
262
+ # Internals
263
+ # ------------------------------------------------------------------
264
+
265
+ def _set_mouse_modifier_mask_unlocked(self, mask: int) -> None:
266
+ mask = int(mask)
267
+ if mask != self._mouse_modifier_mask:
268
+ self._mouse_modifier_mask = mask
269
+ self._mouse_modifier_dirty = True
270
+
271
+ def _clamp(self, x: int, y: int) -> Tuple[int, int]:
272
+ with self._lock:
273
+ w = self._width
274
+ h = self._height
275
+ out_of_range = x < 0 or y < 0 or x > (w - 1) or y > (h - 1)
276
+ clamped_x = min(max(int(x), 0), w - 1)
277
+ clamped_y = min(max(int(y), 0), h - 1)
278
+ if out_of_range:
279
+ with self._lock:
280
+ fire_warn = not self._out_of_range_warned
281
+ self._out_of_range_warned = True
282
+ if fire_warn:
283
+ _LOG.warning(
284
+ "coord out of range (%d,%d) for extents %dx%d; "
285
+ "clamping to (%d,%d). Subsequent out-of-range events will be silent.",
286
+ x, y, w, h, clamped_x, clamped_y,
287
+ )
288
+ return clamped_x, clamped_y
@@ -0,0 +1,232 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """Tier 3 input bridge → ovui native input dispatcher.
12
+
13
+ The :class:`ovui_widgets.app._input_bridge.RemoteInputBridge` accumulates events
14
+ on the ovstream worker thread; this module is the main-loop side that
15
+ translates them into ``omni.ui._ui._inject_*`` calls.
16
+
17
+ Critical ordering: the dispatcher must run **before**
18
+ ``await ui.next_frame()`` so the injected events land in *that* tick's
19
+ ImGui IO. ovui's ``HeadlessVulkanPlatform.cpp`` runs
20
+ ``applyInjectedInput()`` at line 485, immediately before
21
+ ``ImGui::NewFrame()`` at line 487; an event injected after the
22
+ ``next_frame`` await would not be seen until the *next* tick.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Callable, Optional
28
+
29
+ from ovstream import KeyboardEvent, KeyState, MouseEvent, MouseEventType
30
+
31
+ from ovui_widgets.app._input_bridge import RemoteInputBridge
32
+ from ovui_widgets.app._input_keymap import nvst_to_imgui_key
33
+
34
+ # ImGui's explicit modifier-key slots. These are inside
35
+ # ImGuiKey_NamedKey_BEGIN..END, so the existing standalone _inject_key_event
36
+ # binding accepts them without needing a native API change.
37
+ _IMGUI_KEY_RESERVED_MOD_CTRL = 663
38
+ _IMGUI_KEY_RESERVED_MOD_SHIFT = 664
39
+ _IMGUI_KEY_RESERVED_MOD_ALT = 665
40
+ _IMGUI_KEY_RESERVED_MOD_SUPER = 666
41
+
42
+ _NVST_MOD_SHIFT = 0x0001
43
+ _NVST_MOD_CTRL = 0x0002
44
+ _NVST_MOD_ALT = 0x0004
45
+ _NVST_MOD_SUPER = 0x0008
46
+
47
+ # ovstream's ``MouseButton`` enum and ImGui's ``ImGuiMouseButton_`` enum
48
+ # disagree on the order of the middle and right buttons:
49
+ #
50
+ # ovstream (NONE=0, LEFT=1, MIDDLE=2, RIGHT=3, EXTRA1=4, EXTRA2=5)
51
+ # ImGui (Left=0, Right=1, Middle=2, …) — imgui.h:2042–2047
52
+ #
53
+ # ovui's ``injectMouseButton(int button, bool pressed)`` passes the int
54
+ # straight to ``io.AddMouseButtonEvent`` (StandaloneInit.cpp:330–336),
55
+ # so the integer fed in IS the ImGui button index. Use an explicit
56
+ # table — a subtract-one shortcut would silently swap MIDDLE and RIGHT.
57
+ _NVST_TO_IMGUI_BUTTON: dict[int, int] = {
58
+ 1: 0, # MouseButton.LEFT → ImGuiMouseButton_Left (0)
59
+ 3: 1, # MouseButton.RIGHT → ImGuiMouseButton_Right (1)
60
+ 2: 2, # MouseButton.MIDDLE → ImGuiMouseButton_Middle (2)
61
+ 4: 3, # MouseButton.EXTRA1 → 3
62
+ 5: 4, # MouseButton.EXTRA2 → 4
63
+ }
64
+
65
+
66
+ def _nvst_printable_char(key_code: int, modifiers: int) -> str:
67
+ """Return the printable character for *key_code* under *modifiers*, or ''.
68
+
69
+ The NVST SDK delivers keyboard events as physical key codes only; ImGui's
70
+ InputText widgets additionally require ``io.AddInputCharactersUTF8`` to
71
+ insert a glyph into the edit buffer. This function synthesises that
72
+ character for the printable ASCII subset of the NVST code space:
73
+
74
+ 0x20 → space
75
+ 0x30–0x39 → digits 0–9 (identical to ASCII code points)
76
+ 0x41–0x5A → letter keys A–Z (NVST always uses uppercase VK codes)
77
+
78
+ Shift state is bit 0 of *modifiers* (``kModShift``, KeyDefs.h).
79
+ CapsLock state is bit 4 (``kModCapsLock``). Letters are uppercase when
80
+ Shift XOR CapsLock is set, matching standard keyboard behaviour.
81
+
82
+ Control (bit 1, ``kModControl``) and Alt (bit 2, ``kModAlt``) suppress
83
+ character synthesis — Ctrl+key and Alt+key combos are accelerators, not
84
+ text input.
85
+ """
86
+ if modifiers & 0x0006: # kModControl | kModAlt
87
+ return ''
88
+ if key_code == 0x0020:
89
+ return ' '
90
+ if 0x0030 <= key_code <= 0x0039:
91
+ return chr(key_code)
92
+ if 0x0041 <= key_code <= 0x005A:
93
+ shift = bool(modifiers & 0x0001)
94
+ caps = bool(modifiers & 0x0010)
95
+ return chr(key_code) if (shift ^ caps) else chr(key_code + 32)
96
+ return ''
97
+
98
+
99
+ def _inject_imgui_modifier_state(ui_native: Any, modifiers: int) -> None:
100
+ """Submit ImGui's aggregate modifier state from an NVST modifier mask.
101
+
102
+ The livestream SDK may report Alt/Ctrl/Shift/Super on mouse events even
103
+ when it does not deliver separate keyboard events. Scene gestures compare
104
+ against ImGui's aggregate modifier slots (``io.KeyAlt`` etc.), so injecting
105
+ only the physical key is not enough for Alt+LMB camera navigation.
106
+ """
107
+ mask = int(modifiers)
108
+ ui_native._inject_key_event(
109
+ _IMGUI_KEY_RESERVED_MOD_CTRL,
110
+ bool(mask & _NVST_MOD_CTRL),
111
+ )
112
+ ui_native._inject_key_event(
113
+ _IMGUI_KEY_RESERVED_MOD_SHIFT,
114
+ bool(mask & _NVST_MOD_SHIFT),
115
+ )
116
+ ui_native._inject_key_event(
117
+ _IMGUI_KEY_RESERVED_MOD_ALT,
118
+ bool(mask & _NVST_MOD_ALT),
119
+ )
120
+ ui_native._inject_key_event(
121
+ _IMGUI_KEY_RESERVED_MOD_SUPER,
122
+ bool(mask & _NVST_MOD_SUPER),
123
+ )
124
+
125
+
126
+ def _mouse_scroll_delta(event: MouseEvent) -> tuple[float, float]:
127
+ """Return the wheel delta, preferring modern ovstream float fields."""
128
+ scroll_x = float(getattr(event, "scroll_x", 0.0) or 0.0)
129
+ scroll_y = float(getattr(event, "scroll_y", 0.0) or 0.0)
130
+ if scroll_x != 0.0 or scroll_y != 0.0:
131
+ return scroll_x, scroll_y
132
+ return float(event.data), float(event.data2)
133
+
134
+
135
+ def drain_bridge_into_ui(
136
+ bridge: RemoteInputBridge,
137
+ ui_native: Any,
138
+ *,
139
+ on_left_click: Optional[Callable[[int, int], None]] = None,
140
+ on_char: Optional[Callable[[str], None]] = None,
141
+ ) -> None:
142
+ """Drain *bridge* and inject every event into *ui_native*.
143
+
144
+ *ui_native* is the ``omni.ui._ui`` C-binding submodule (or any
145
+ object exposing the same ``_inject_*`` surface — a mock works for
146
+ tests).
147
+
148
+ Mouse-position injection is unconditional: the latest cursor
149
+ coordinates always go in first so subsequent button/wheel events
150
+ fire at the up-to-date hover target. Discrete events are dispatched
151
+ in the order they were enqueued.
152
+
153
+ The function never raises on bridge contents — unmappable keys
154
+ (``ImGuiKey_None``), the ``MouseButton.NONE`` button, and out-of-
155
+ range button indices are silently dropped rather than fed into the
156
+ ImGui IO.
157
+
158
+ *on_left_click*, if provided, is called with the current ``(x, y)``
159
+ stream coordinates whenever a left-button DOWN event is drained. The
160
+ application uses this to synthesise widget focus for widgets (such as
161
+ the Stage Browser filter bar) whose ``set_mouse_pressed_fn`` callbacks
162
+ do not fire from ImGui IO injection alone. The callback runs before
163
+ ``ui.next_frame()``, so any programmatic focus changes it makes land
164
+ in the same tick.
165
+
166
+ *on_char*, if provided, is called with each printable ASCII character
167
+ synthesised from a key-down event (via :func:`_nvst_printable_char`).
168
+ The application uses this to route typed characters to widget models
169
+ (e.g. the Stage Browser filter) that require direct model updates
170
+ because ``io.AddInputCharactersUTF8`` alone is insufficient when the
171
+ target widget hasn't captured ImGui keyboard focus.
172
+ """
173
+ (mouse_x, mouse_y), events = bridge.drain()
174
+ if hasattr(bridge, "consume_mouse_modifier_state"):
175
+ mouse_modifiers, modifiers_changed = bridge.consume_mouse_modifier_state()
176
+ if modifiers_changed:
177
+ _inject_imgui_modifier_state(ui_native, mouse_modifiers)
178
+ elif hasattr(bridge, "mouse_modifier_mask"):
179
+ mouse_modifiers = bridge.mouse_modifier_mask()
180
+ if mouse_modifiers:
181
+ _inject_imgui_modifier_state(ui_native, mouse_modifiers)
182
+ ui_native._inject_mouse_move(int(mouse_x), int(mouse_y))
183
+
184
+ for event in events:
185
+ if isinstance(event, MouseEvent):
186
+ if event.modifiers:
187
+ _inject_imgui_modifier_state(ui_native, int(event.modifiers))
188
+ if event.type == MouseEventType.BUTTON:
189
+ imgui_button = _NVST_TO_IMGUI_BUTTON.get(int(event.data))
190
+ # ``MouseButton.NONE`` (0) and any future SDK addition
191
+ # outside the table drop silently — feeding an
192
+ # unrecognised index into ovui would corrupt
193
+ # ``MouseDown[]`` (the inject helper takes the int
194
+ # verbatim and indexes a fixed-size array).
195
+ if imgui_button is not None:
196
+ ui_native._inject_mouse_button(
197
+ imgui_button,
198
+ event.button_state == KeyState.DOWN,
199
+ )
200
+ if (imgui_button == 0
201
+ and event.button_state == KeyState.DOWN
202
+ and on_left_click is not None):
203
+ on_left_click(int(mouse_x), int(mouse_y))
204
+ if event.button_state == KeyState.UP and event.modifiers:
205
+ _inject_imgui_modifier_state(ui_native, 0)
206
+ continue
207
+ if event.type == MouseEventType.WHEEL:
208
+ ui_native._inject_mouse_scroll(*_mouse_scroll_delta(event))
209
+ continue
210
+ # MouseEventType.MOVE never reaches the deque (it coalesces
211
+ # into the atomic position read above). Defensive: skip
212
+ # silently rather than raise if the bridge ever changes.
213
+ continue
214
+
215
+ if isinstance(event, KeyboardEvent):
216
+ imgui_key = nvst_to_imgui_key(int(event.key_code))
217
+ if imgui_key != 0:
218
+ ui_native._inject_key_event(
219
+ imgui_key,
220
+ event.key_state == KeyState.DOWN,
221
+ )
222
+ if event.key_state == KeyState.DOWN:
223
+ ch = _nvst_printable_char(int(event.key_code), int(event.modifiers))
224
+ if ch:
225
+ ui_native._inject_text_input(ch)
226
+ if on_char is not None:
227
+ on_char(ch)
228
+ continue
229
+
230
+ if isinstance(event, str):
231
+ ui_native._inject_text_input(event)
232
+ continue