Skip to content

Commit

Permalink
first pass (#1)
Browse files Browse the repository at this point in the history
* first pass

* remove unnecessary dep

* document component
  • Loading branch information
bmanturner authored Nov 21, 2022
1 parent 303f472 commit daf8d15
Show file tree
Hide file tree
Showing 8 changed files with 521 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.DS_Store
161 changes: 160 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,160 @@
# omni-component
# HEAVY.AI | Omni.Ui Component System

This component system was first presented during a NVIDIA Partner Spotlight with HEAVY.AI and can be found [here on YouTube](https://youtu.be/QhBMgx2G86g?t=1640)

![Slide presented during NVIDIA Omniverse Partner Spotlight](./exts/heavyai.ui.component/resources/partner-spotlight-slide.png)

## Goals

1. Provide a means to encapsulate functions and `omni.ui` widgets to modularize UI code by its purpose to enforce boundaries using separation of concerns
2. Standardize the creation and implementation of UI components in a manner that enforces self-documentation and a homogenous UI codebase
3. Provide the utility of updating and re-rendering specific components without re-rendering components that have not changed



## Examples

### Hello World

```python
from heavyai.ui.component import Component

class SimpleLabel(Component):
def render(self):
ui.Label("Hello World")

# USAGE
SimpleLabel()

class CustomLabel(Component):
value: str = "Hello World"

def render(self):
ui.Label(self.value)

# USAGE
CustomLabel(value="Hello There")
```

### Internal State

```python
from heavyai.ui.component import Component

class ButtonCounter(Component):
value: int = 0
on_change: Optional[Callable[[int], None]]

def _handle_click(self):
self.value += 1
if self.on_change:
self.on_change(self.value)
self.update()

def render(self):
with self.get_root(ui.HStack):
ui.Label(str(self.value))
ui.Button("Increment", clicked_fn=self._handle_click()

# USAGE
ButtonCounter(on_change=lambda val: print(val))
```

### Subscribing Components

```python
from heavyai.ui.component import Component

class UsdSubscribingComponent(Component):
prim: pxr.Usd.Prim

def __init__(self, **kwargs):
super().__init__(**kwargs)

self.subscription: carb.Subscription = (
usd.get_watcher().subscribe_to_change_info_path(
path=self.prim.GetPath(), on_change=self._handle_prim_change
)
)

def _handle_prim_change(self, prim_attr_path):
self.update()

def render(self):
with self.get_root(ui.VStack):
pass

def destroy(self):
self.subscription.unsubscribe()


class AbstractModelSubscribingComponent(Component):
model: sc.AbstractManipulatorModel

def __init__(self, **kwargs):
super().__init__(**kwargs)

self.subscription: carb.Subscription = (
self.model.subscribe_item_changed_fn(
self._handle_model_item_changed
)
)

def _handle_model_item_changed(self, model, item):
self.update()

def render(self):
with self.get_root(ui.VStack):
pass

def destroy(self):
self.subscription.unsubscribe()
```

## Re-rendering “In Place”

Similar to the ability to update the `style` property of any `ui.Widget` element or the `text` property of `ui.Label` and `ui.Button` elements, `Components` can also be updated after they are created. Components can update themselves in response to subscribed events or user interactions, or components can be updated elsewhere via reference. This *drastically* increases the composability of UI elements.

### Using Setters

```python
class CustomLabel(Component):
value: str = "Hello World"

def set_value(self, value: str):
self.value = value
self.update()

def render(self):
ui.Label(self.value)

hello_world_label = CustomLabel()
hello_world_label.set_value(
"Goodbye, and see you later"
)
```

### Using a Reference

```python
hello_world_label = CustomLabel()

hello_world_label.value = "Goodbye, and see you later"
hello_world_label.update()
```

## Caveat: Destroying a Component

Components are not automatically deleted when cleared from a container (or should otherwise be destroyed), so if a `destroy()` method is provided, a reference to that component must be stored and destroyed by the component’s “parent” when appropriate, resulting in a chain of saved references and destroy calls. This is often the case when a component sets up a subscription; the subscription must be unsubscribed when the component is removed. See 'Subscribing Components' above for examples of components that require a `destroy` method.

```python
class ParentComponent(Component):
def render(self):
prim = # get prim reference

with self.get_root(ui.VStack):
self.child = UsdSubscribingComponent(prim=prim)

def destroy(self):
self.child.destroy()
```
36 changes: 36 additions & 0 deletions exts/heavyai.ui.component/config/extension.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
# Semantic Versionning is used: https://semver.org/
version = "1.0.0"

# Lists people or organizations that are considered the "authors" of the package.
authors = ["HEAVY.AI", "Brendan Turner", "Chris Matzenbach"]

# The title and description fields are primarily for displaying extension info in UI
title = "Heavy Omni.UI Component System"
description="Component class for UI organization and encapsulation"

# Path (relative to the root) or content of readme markdown file for UI.
readme = "./docs/README.md"

# URL of the extension source repository.
repository = "https://github.com/heavyai/omni-component"

# One of categories for UI.
category = "Rendering"

# Keywords for the extension
keywords = ["heavyai", "component", "react"]

# Preview image is shown in "Overview" of Extensions window. Screenshot of an extension might be a good preview image.
preview_image = "resources/partner-spotlight-slide.png"

# Icon is shown in Extensions window, it is recommended to be square, of size 256x256.
icon = "resources/logo_black.png"

# Use omni.ui to build simple UI
[dependencies]
"omni.ui" = {}

# Main python module this extension provides, it will be publicly available as "import omni.hello.world".
[[python.module]]
name = "heavyai.ui.component"
162 changes: 162 additions & 0 deletions exts/heavyai.ui.component/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# HEAVY.AI | Omni.Ui Component System

This component system was first presented during a NVIDIA Partner Spotlight with HEAVY.AI and can be found [here on YouTube](https://youtu.be/QhBMgx2G86g?t=1640)

![Slide presented during NVIDIA Omniverse Partner Spotlight](../resources/partner-spotlight-slide.png)

## Goals

1. Provide a means to encapsulate functions and `omni.ui` widgets to modularize UI code by its purpose to enforce boundaries using separation of concerns
2. Standardize the creation and implementation of UI components in a manner that enforces self-documentation and a homogenous UI codebase
3. Provide the utility of updating and re-rendering specific components without re-rendering components that have not changed



## Examples

### Hello World

```python
from heavyai.ui.component import Component

class SimpleLabel(Component):
def render(self):
ui.Label("Hello World")

# USAGE
SimpleLabel()

class CustomLabel(Component):
value: str = "Hello World"

def render(self):
ui.Label(self.value)

# USAGE
CustomLabel(value="Hello There")
```

### Internal State

```python
from heavyai.ui.component import Component

class ButtonCounter(Component):
value: int = 0
on_change: Optional[Callable[[int], None]]

def _handle_click(self):
self.value += 1
if self.on_change:
self.on_change(self.value)
self.update()

def render(self):
with self.get_root(ui.HStack):
ui.Label(str(self.value))
ui.Button("Increment", clicked_fn=self._handle_click()

# USAGE
ButtonCounter(on_change=lambda val: print(val))
```

### Subscribing Components

```python
from heavyai.ui.component import Component

class UsdSubscribingComponent(Component):
prim: pxr.Usd.Prim

def __init__(self, **kwargs):
super().__init__(**kwargs)

self.subscription: carb.Subscription = (
usd.get_watcher().subscribe_to_change_info_path(
path=self.prim.GetPath(), on_change=self._handle_prim_change
)
)

def _handle_prim_change(self, prim_attr_path):
self.update()

def render(self):
with self.get_root(ui.VStack):
pass

def destroy(self):
self.subscription.unsubscribe()


class AbstractModelSubscribingComponent(Component):
model: sc.AbstractManipulatorModel

def __init__(self, **kwargs):
super().__init__(**kwargs)

self.subscription: carb.Subscription = (
self.model.subscribe_item_changed_fn(
self._handle_model_item_changed
)
)

def _handle_model_item_changed(self, model, item):
self.update()

def render(self):
with self.get_root(ui.VStack):
pass

def destroy(self):
self.subscription.unsubscribe()
```

## Re-rendering “In Place”

Similar to the ability to update the `style` property of any `ui.Widget` element or the `text` property of `ui.Label` and `ui.Button` elements, `Components` can also be updated after they are created. Components can update themselves in response to subscribed events or user interactions, or components can be updated elsewhere via reference. This *drastically* increases the composability of UI elements.

### Using Setters

```python
class CustomLabel(Component):
value: str = "Hello World"

def set_value(self, value: str):
self.value = value
self.update()

def render(self):
ui.Label(self.value)

hello_world_label = CustomLabel()
hello_world_label.set_value(
"Goodbye, and see you later"
)
```

### Using a Reference

```python
hello_world_label = CustomLabel()

hello_world_label.value = "Goodbye, and see you later"
hello_world_label.update()
```

## Caveat: Destroying a Component

Components are not automatically deleted when cleared from a container (or should otherwise be destroyed), so if a `destroy()` method is provided, a reference to that component must be stored and destroyed by the component’s “parent” when appropriate, resulting in a chain of saved references and destroy calls. This is often the case when a component sets up a subscription; the subscription must be unsubscribed when the component is removed. See 'Subscribing Components' above for examples of components that require a `destroy` method.

```python
class ParentComponent(Component):
def render(self):
prim = # get prim reference

with self.get_root(ui.VStack):
self.child = UsdSubscribingComponent(
prim=prim
)

def destroy(self):
self.child.destroy()
```
1 change: 1 addition & 0 deletions exts/heavyai.ui.component/heavyai/ui/component/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .extension import *
Loading

0 comments on commit daf8d15

Please sign in to comment.