Chris Sprance

Technical Art Director

SnoUI

VS CodeVS Code
PythonPython

A Python library for building tool UIs inside Snowdrop without writing verbose PySide boilerplate. Originally developed during The Division: Heartland and used across multiple Snowdrop titles including XDefiant.

The Problem

Most pipeline scripts need the same thing: a handful of inputs, a button, and something happens. But writing that in raw PySide means dozens of lines of widget instantiation, layout management, and signal wiring before you've written a single line of actual tool logic. The UI ends up overwhelming the code it exists to serve.

The Solution

SnoUI flips the model. You describe your UI in a YAML string — just the fields you need, nothing else — and SnoUI builds it. State management follows a React-style get_state / set_state pattern, and buttons are wired to tool functions with a single connect() call. The result is that your tool logic stays front and center, and the UI is just a short block of markup above it.

YAML was a deliberate choice: it's minimal, human-readable, supports arrays natively, and has almost no syntax to learn. The goal was zero barrier to adoption for pipeline TDs who just need to ship a tool.

Define it, connect it, run it:

my_cool_radio:
    type: radio
    value: [A, B, See, Dee]
    label: Radio Group
div1:
    type: divider
label:
    type: label
    label: This is a label
my_cool_dropdown:
    type: dropdown
    label: Dropdown
    value: ['Item1', 'Item2']
my_cool_button:
    type: button
    label: Joined To Next
    join: True

Full Example

The example below covers the full feature set — state management, visibility toggling, custom components, collapsible sections, and the debug pattern for testing tool logic without rendering the UI.

UI Example

"""
This example demonstrates how to render all the UI elements available:

Writing the UI separately from the tool code helps keep things about
the tool not the ui.
"""
import random
from datetime import datetime
from random import randint, shuffle
from typing import Optional

from python3.code_generation.generated_classes import SRSX_FMWidgetTransferBox
from python3.utilities.snoui import SnoUI, SnoUIComponent
from python3.utilities.snoui.utils import label
from python3.utilities.ui_utilities import create_collapsible_frame
from sdvectormath import Matrix44


def my_cool_random_value_tool(ui: SnoUI, signal_name, context):
    """
    This function is bound in the my_example_ui function with connect('my_cool_random_button', my_cool_random_button)
    This button just causes the ui to set a bunch of random values to illustrate how to set state
    Make sure to have ui be an argument in your function! The : SnoUI bit after the arg is a nice way to get
    autocomplete for all the methods you can use with snoui
    :param ui: The current SnoUI instance
    :param signal_name: The name of the signal that triggered this function
    :param context: A context Dict that is populated with some data like 'name' which is the name of the calling
    component (What is defined in yml)
    :return:
    """
    # We can key things off the signal name
    print(signal_name)
    # Data about how this signal was called will exist in this dict
    print(context)

    # This field is parsed so it allows math expressions
    ui.log(ui.get_state('my_cool_vec3s'))

    # we can use data components to just store arbitrary data values
    # here we just log the default then set it to something else
    # and then log the changed value
    ui.log(ui.get_state('my_data_component'))
    ui.set_state('my_data_component', {'a dict key': 'some dict val'})
    ui.log(ui.get_state('my_data_component'))

    def randint_array(num):
        return [randint(0, 100) for _ in range(num)]

    def shuffle_and_return(_list):
        _l = _list
        shuffle(_l)
        return _l

    ui.set_state(
        "my_cool_mat44",
        Matrix44(
            randint_array(4), randint_array(4), randint_array(4), randint_array(4)
        ),
    )
    ui.set_state("my_cool_string", "Random Values")
    ui.set_state("my_cool_checkbox", False)
    ui.set_state("my_cool_vector3", randint_array(3))
    ui.set_state("my_cool_vector4", randint_array(4))
    ui.set_state("my_cool_vector2", randint_array(2))
    ui.set_state("my_cool_float", randint(0, 100))
    ui.set_state("my_cool_slider", randint(0, 10))
    ui.set_state(
        "my_cool_dropdown", shuffle_and_return(ui.get_state("my_cool_dropdown"))
    )

    # Sometimes we need to manually save the state to the database because it isn't called on any form events except
    # when we explicitly get or set state through the middleware triggered by our tool function.
    # Only works if you add persist_key='some-key'
    # ui.persist()


def my_cool_tool(ui: SnoUI, signal_name):
    """
    This is more of the tool code you're writing the UI for it is important, and we should focus our attention
    here rather than writing verbose faceman/pyside code That's why it's usually good practice to put the business logic
    up at the top of the file
    """
    # Fetch a value from the ui
    a_string_val = ui.get_state("my_cool_string")
    # If you have a log component in your ui these will show up there
    ui.logger.add_error(a_string_val)
    # We can also set the state of a ui component for things like clearing or defaulting values
    ui.set_state(
        "my_cool_string", f"State Set: {datetime.now()}! Signal Name: {signal_name}"
    )
    ui.set_state('my_progress', random.random())
    ui.set_state(
        'my_nested_comp_a', f"State Set: {datetime.now()}! Signal Name: {signal_name}"
    )
    ui.set_state('my_nested_comp_b', random.random())
    # Log a value
    ui.log(ui.get_state("my_cool_mat44"))
    ui.log(ui.get_state("my_nested_comp_a"))


def handle_ui_update(ui: SnoUI, signal_name, context):
    """
    We can handle visibility toggles here based on UI state

    :param ui:
    :param signal_name:
    :param context:
    :return:
    """
    # We can create little one off state objects like this to use just like react.useState(initialState)
    # use_state returns a tuple of (value, setter)
    [viz_a, set_viz_a] = ui.use_state(True)
    [viz_b, set_viz_b] = ui.use_state(False)

    # Check the name of the component that triggered this signal to be emitted
    # This lets you wrap multiple logic paths in one function
    if context.get('name') == 'my_cool_viz_toggle_button':
        ui.set_visible('my_cool_vec3s', set_viz_a(not viz_a))

    if context.get('name') == 'my_cool_viz_toggle_button2':
        ui.set_visible('my_cool_string_2', set_viz_b(not viz_b))


class CustomComponent(SnoUIComponent):
    """
    We can use a custom component for times when we need something more advanced.
    Just create it wrap it in a hbox or vbox and use it by name in the yaml below
    """

    # You always need to override build() at least.
    def build(self) -> SRSX_FMWidgetTransferBox:
        # Create the faceman component
        contents = []
        t = label(self.kwargs.get("value"))
        contents.append(t)
        # Add the widget to the widget dict this is used for visibility
        self.widgets['label'] = t.get_widget()

        # Can also set it as an attribute on the class
        self.label_widget = self.widgets['label']

        collapsible_t, w = create_collapsible_frame(contents, self.kwargs.get("label"))
        # return the TransferBox or wrapper transfer box
        return collapsible_t

    # if we'd like to get and set state we also need to define those with the correct args
    def get_state(self) -> Optional[bool]:
        # we can use the attribute here we set
        return self.label_widget.get_text()

    def set_state(self, state: bool) -> None:
        # or use the widgets dict
        self.widgets['label'].set_text(state)


def my_example_ui():
    """
    This is the main function we run that builds and shows the ui
    We define how our ui looks in yaml
    Then connect our tool code functions
    """
    # This is the actual UI layout yml code that describes our entire ui
    #  This could also be a python dict
    ui_text = """
my_cool_radio:
    type: radio
    value: [A, B, See, Dee]
    label: Radio Group
my_cool_mat44:
    type: matrix44
    label: My Cool Matrix44
    value: [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]
my_cool_string:
    type: string
    label: My Cool String
    value: Default Value
my_cool_checkbox:
    type: checkbox
    label: My Cool Checkbox
    value: false
div2:
    type: divider
my_cool_vector3:
    type: vector3
    label: My Cool Vec3
    value: [1.5, 0, 1]
my_cool_vector4:
    type: vector4
    label: My Cool Vec4
    value: [1.5, 0, 1, 1]
my_cool_vector2:
    type: vector2
    label: My Cool Vec2
    value: [1.5, 0]
my_cool_float:
    type: float
    label: My Cool Float
    value: 1.5
my_cool_label:
    type: label
    label: Label For Slider Joined 
    join: True
my_cool_slider:
    type: slider
    min: 0
    max: 10
    step: .5
    value: 5
div1:
    type: divider
label:
    type: label
    label: This is a label
my_cool_dropdown:
    type: dropdown
    label: Dropdown
    value: ['Item1', 'Item2']
my_cool_button:
    type: button
    label: Joined To Next
    join: True
my_cool_random_button:
    type: button
    label: Random Values
    join: True
my_cool_viz_toggle_button:
    type: button
    label: I Toggle Visibility
    join: true
my_cool_viz_toggle_button2:
    type: button
    label: I Also Toggle Visibility
my_cool_vec3s:
    type: vector3s
    label: Vector3 String
    value: [a, b, c]
my_cool_string_2:
    type: string
    label: A String That Shows up
    value: I just showed up!
    visible: False
my_progress:
    type: progress
my_custom_component:
    type: my_custom_component
    label: Custom Component
    value: [1,2,3]
my_data_component:
    type: data
    value: whatever you want
my_collapsible:
    type: collapsible
    label: Collapsible 1
    children:
        my_nested_comp_a:
            type: string
            label: String Component A
            value: A String Component A
        my_nested_comp_b:
            type: float
            label: Float Component B
            value: 1.0
my_collapsible2:
    type: collapsible
    label: Collapsible 2
    children:
        my_nested_comp_c:
            type: string
            label: String Component C
            value: A String Component C
        my_nested_comp_d:
            type: float
            label: Float Component D
            value: 1.0
"""
    # First we instantiate the ui
    ui = SnoUI(
        "My Cool Script Title",
        ui_text,
        log=True,  # adding log=True is usually a good idea
        # Add in a dict with the type of the component and the Class Reference to add custom components
        components={'my_custom_component': CustomComponent},
        # persist_key="example-snoui:2.0.4",  # Optionally enable persistence with a unique persist key
    )

    # Then each button can be 'connected' like this by name to a function
    # When the function is connected it will have the ui instance above passed in to it for you
    ui.connect("my_cool_button", my_cool_tool)
    ui.connect("my_cool_random_button", my_cool_random_value_tool)
    ui.connect("my_cool_viz_toggle_button", handle_ui_update)
    ui.connect("my_cool_viz_toggle_button2", handle_ui_update)

    # Finally show the UI
    ui.render()  # Comment me out to debug

    # DEBUG HOW TO
    #  If we are working on a script and we need to run it using the same inputs to test, it is much easier to
    #  Comment out the ui.render() function above then explicitly set the state manually and then
    #  call the tool code function needed passing in the SnoUI instance

    # Uncomment below to debug
    # ui.set_state('my_cool_vector4', [1, 2, 3, 4])
    # ui.set_state('my_cool_vector2', [1, 2])
    # my_cool_tool(ui)


# To kick the whole thing off we run the function that calls ui.render()
my_example_ui()