Skip to content

Style, Colors and UI

FluxRender.ui.Button

Button(text: str, on_click: Callable, position: Sequence[int] = (0, 50), width: int = 150, height: int = 50, align: Align = Align.LEFT_TOP, style: UIStyle = None)

Bases: UIWidget

Represents an interactive button widget in the UI system.

This class extends UIWidget to provide a clickable element with a text label. It manages its own visual state transitions (idle, hover, active) and efficiently updates GPU-accessible color fields (ti.field) for rendering.

Key Features:

  • Dynamic Styling: Automatically updates text and background colors based on mouse interaction.
  • Smart Callbacks: Inspects the provided on_click handler to optionally pass the button instance as an argument.
  • Text Rendering: Handles text texture baking for the button label.
  • Alignment: Supports various anchor points (e.g., Center, Top-Left) for flexible positioning.

Parameters:

Name Type Description Default
text str

The label text displayed on the button.

required
on_click callable

The function to execute when the button is clicked. This function can accept 0 arguments or 1 argument (the Button instance).

required
position Sequence[int]

The (x, y) coordinates for the button's anchor point. Defaults to (0, 50).

(0, 50)
width int

The width of the button in pixels. Defaults to 150.

150
height int

The height of the button in pixels. Defaults to 50.

50
align Align

The alignment anchor point relative to the position coordinates (e.g., LEFT_TOP, CENTER). Defaults to Align.LEFT_TOP.

LEFT_TOP
style UIStyle

A styling object defining colors and fonts. If None, default styling is used.

None
Example

Creating a button that changes the color_property of a VectorField when clicked:

import FluxRender as fr

# [Initializing the scene and coordinate system]

# Create a vector field
vector_field = fr.VectorField(vec_function=lambda x, y: (y, -x))

# Define function to toggle vector field color
def toggle_color():
    if vector_field.color_property == fr.Property.VELOCITY:
        vector_field.color_property = fr.Property.DIVERGENCE
    else:
        vector_field.color_property = fr.Property.VELOCITY

# Create a button with the toggle function
button = fr.Button(
    text = "Change Property",
    on_click = toggle_color,
    style = fr.UIStyle(
        font_size=15
    )
)

scene.add(vector_field, button)

Source code in FluxRender/ui.py
def __init__(self,
             text: str,
             on_click: Callable,
             position: Sequence[int] = (0, 50),
             width: int = 150,
             height: int = 50,
             align: Align = Align.LEFT_TOP,
             style: UIStyle = None
             ):
    """
    Args:
        text (str): The label text displayed on the button.
        on_click (callable): The function to execute when the button is clicked.
            This function can accept 0 arguments or 1 argument (the Button instance).
        position (Sequence[int], optional): The (x, y) coordinates for the button's anchor point. Defaults to (0, 50).
        width (int, optional): The width of the button in pixels. Defaults to 150.
        height (int, optional): The height of the button in pixels. Defaults to 50.
        align (Align, optional): The alignment anchor point relative to the position coordinates
            (e.g., LEFT_TOP, CENTER). Defaults to Align.LEFT_TOP.
        style (UIStyle, optional): A styling object defining colors and fonts.
            If None, default styling is used.

    Example:
        Creating a button that changes the color_property of a VectorField when clicked:
        ```python
        import FluxRender as fr

        # [Initializing the scene and coordinate system]

        # Create a vector field
        vector_field = fr.VectorField(vec_function=lambda x, y: (y, -x))

        # Define function to toggle vector field color
        def toggle_color():
            if vector_field.color_property == fr.Property.VELOCITY:
                vector_field.color_property = fr.Property.DIVERGENCE
            else:
                vector_field.color_property = fr.Property.VELOCITY

        # Create a button with the toggle function
        button = fr.Button(
            text = "Change Property",
            on_click = toggle_color,
            style = fr.UIStyle(
                font_size=15
            )
        )

        scene.add(vector_field, button)
        ```
    """

    self._is_initialized = False
    self._is_initialized_shape = False

    super().__init__(position, width, height, align, style)

    self._current_background_color = ti.Vector.field(4, dtype=float, shape=())
    self._current_text_color = ti.Vector.field(4, dtype=float, shape=())

    self.is_active = False

    self.text = text
    self.on_click = on_click

    self._is_dirty = True
    self._last_applied_bg = None
    self._last_applied_txt = None
    self._was_lmb_down = False

    self.scene = None

    # checking the number of parameters
    self._params = _count_function_parameters(on_click)
    if self._params > 1 and self._params != float('inf'):
        _fatal_error(f"on_click function must have 0, 1, or unlimited parameters (*args or **kwargs). Got {self._params}.", "ValueError")

    self.is_active = False
    self._is_initialized = True
    self._bake_text_texture()

change_text

change_text(new_text: str) -> None

Changes the button's text and updates the texture.

Parameters:

Name Type Description Default
new_text str

The new text to display on the button.

required
Source code in FluxRender/ui.py
def change_text(self, new_text: str) -> None:
    """
    Changes the button's text and updates the texture.

    Args:
        new_text (str): The new text to display on the button.
    """
    self.text = new_text
    self._bake_text_texture()

is_hovered

is_hovered(scene: Scene) -> bool

A method to check if the cursor position matches the button area

Parameters:

Name Type Description Default
scene Scene

Scene object

required

Returns:

Name Type Description
bool bool

True if the mouse is hovering over the button, False otherwise

Source code in FluxRender/ui.py
def is_hovered(self, scene: cr.Scene) -> bool:
    """
    A method to check if the cursor position matches the button area

    Args:
        scene (Scene): Scene object

    Returns:
        bool: True if the mouse is hovering over the button, False otherwise
    """
    mx, my = scene.mouse_pos

    if mx >= self.min_x and mx <= self.max_x and my >= self.min_y and my <= self.max_y: return True
    return False

FluxRender.ui.DynamicText

DynamicText(text: str | Callable, position: Sequence[int] = (20, 100), align: Align = Align.LEFT_TOP, style: UIStyle = None, width: int | None = None, height: int | None = None)

Bases: UIWidget

A highly customizable UI widget designed for rendering text that can update dynamically.

This widget behaves similarly to a standard UI container or button, supporting rich styling options such as backgrounds, text colors, font sizes, text strokes, padding, and rounded corners. It utilizes an anchor-based alignment system to position itself precisely on the screen. The widget features an intelligent sizing layout: it can either force strict physical dimensions or automatically adapt its bounding box to fit the current text content and padding.

Parameters:

Name Type Description Default
text str | Callable

The static text string to display, or a zero-argument callable (e.g., a lambda function) that provides a dynamically updating string every frame.

required
position Sequence[int]

The absolute (x, y) screen coordinates serving as the spatial anchor point for the widget. Defaults to (20, 100).

(20, 100)
align Align

The alignment behavior relative to the position anchor (e.g., Align.LEFT_TOP, Align.CENTER). Defaults to Align.LEFT_TOP.

LEFT_TOP
style UIStyle

The styling object defining visual aesthetics like background color, text color, font size, text stroke, padding, and border radius. Defaults to None.

None
width int | None

A fixed pixel width for the widget's bounding box. If set to None, the width automatically scales to fit the text length and padding.

None
height int | None

A fixed pixel height for the widget's bounding box. If set to None, the height automatically scales to fit the font metrics and padding.

None
Example

Creating an automatically resizing label that displays a dynamically changing simulation value:

import FluxRender as fr
import numpy as np

# 1. Define the mathematical flow
def flow_vector(x, y):
    X = np.sin(x) * y
    Y = np.cos(y) * x
    return X, Y

# 2. Initialize the automated workspace (gives us a Scene with grids and axes)
scene = fr.create_workspace()

# 3. Create a math engine based on our flow function
math_engine = fr.VectorMathEngine(scene, flow_vector)

# 4. Create vector field (optional)
vortex_vector_field = fr.VectorField(vec_function=flow_vector)


# 5. Set up a DataProbe to track the velocity at the mouse cursor's position
mouse_region = fr.CursorRegion(always_active=True)
probe = fr.DataProbe(
    target_region=mouse_region,
    math_engine=math_engine,
    measured_property=fr.Property.VELOCITY
)


# Display the velocity value at the cursor position using a DynamicText widget
text = fr.DynamicText(
    text=lambda: f"Current Velocity: {probe.value:.2f}",
    position=(20, 600)
)

# 6. Add your entities to the scene and launch!
scene.add(vortex_vector_field, text, mouse_region, probe)
scene.run()

Source code in FluxRender/ui.py
def __init__(self,
             text: str | Callable,
             position: Sequence[int] = (20, 100),
             align: Align = Align.LEFT_TOP,
             style: UIStyle = None,
             width: int | None = None,
             height: int | None = None
             ) -> None:
    """
    Args:
        text (str | Callable): The static text string to display, or a zero-argument callable
            (e.g., a lambda function) that provides a dynamically updating string every frame.
        position (Sequence[int], optional): The absolute (x, y) screen coordinates serving as
            the spatial anchor point for the widget. Defaults to (20, 100).
        align (Align, optional): The alignment behavior relative to the `position` anchor
            (e.g., Align.LEFT_TOP, Align.CENTER). Defaults to Align.LEFT_TOP.
        style (UIStyle, optional): The styling object defining visual aesthetics like background color,
            text color, font size, text stroke, padding, and border radius. Defaults to None.
        width (int | None, optional): A fixed pixel width for the widget's bounding box.
            If set to None, the width automatically scales to fit the text length and padding.
        height (int | None, optional): A fixed pixel height for the widget's bounding box.
            If set to None, the height automatically scales to fit the font metrics and padding.

    Example:
        Creating an automatically resizing label that displays a dynamically changing simulation value:
        ```python
        import FluxRender as fr
        import numpy as np

        # 1. Define the mathematical flow
        def flow_vector(x, y):
            X = np.sin(x) * y
            Y = np.cos(y) * x
            return X, Y

        # 2. Initialize the automated workspace (gives us a Scene with grids and axes)
        scene = fr.create_workspace()

        # 3. Create a math engine based on our flow function
        math_engine = fr.VectorMathEngine(scene, flow_vector)

        # 4. Create vector field (optional)
        vortex_vector_field = fr.VectorField(vec_function=flow_vector)


        # 5. Set up a DataProbe to track the velocity at the mouse cursor's position
        mouse_region = fr.CursorRegion(always_active=True)
        probe = fr.DataProbe(
            target_region=mouse_region,
            math_engine=math_engine,
            measured_property=fr.Property.VELOCITY
        )


        # Display the velocity value at the cursor position using a DynamicText widget
        text = fr.DynamicText(
            text=lambda: f"Current Velocity: {probe.value:.2f}",
            position=(20, 600)
        )

        # 6. Add your entities to the scene and launch!
        scene.add(vortex_vector_field, text, mouse_region, probe)
        scene.run()
        ```
    """


    super().__init__(position, 1, 1, align, style)

    self.text = text
    self._current_text = self.text() if callable(self.text) else self.text

    self._max_chars = 400

    self.color = ti.Vector.field(4, dtype=float, shape=())
    self.background_color = ti.Vector.field(4, dtype=float, shape=())
    self.stroke_color = ti.Vector.field(4, dtype=float, shape=())

    self._char_indices = ti.field(dtype=int, shape=self._max_chars)
    self._char_widths = ti.field(dtype=int, shape=self._max_chars)
    self._char_x_positions = ti.field(dtype=int, shape=self._max_chars)

    self._np_indices = np.zeros(self._max_chars, dtype=np.int32)
    self._np_widths = np.zeros(self._max_chars, dtype=np.int32)
    self._np_x_positions = np.zeros(self._max_chars, dtype=np.int32)

    self._fixed_width = width
    self._fixed_height = height

FluxRender.ui.Axis

Axis(color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), thickness: float = 2.5, label_size: int = 14, label_color: Sequence[float] = (1, 1, 1, 1), label_density: int = 10, label_offset_x: Sequence[float] = (0, -4), label_offset_y: Sequence[float] = (-4, 0), label_offset_0: Sequence[float] = (-4, -4), cover_background: bool = False, antyaliasing: bool = True, draw_arrows: bool = True, arrow_size: float = 25, arrow_style: ArrowStyle = ArrowStyle.HARPOON, arrow_color: Sequence[float] = (0.8, 0.8, 0.8, 1.0))

Bases: Renderable

Renders 2D coordinate axes with dynamic, GPU-accelerated labels.

This class handles the drawing of the main X and Y axes lines using SDF (Signed Distance Fields) for anti-aliasing, and manages the batch rendering of textual labels via a texture atlas. It supports dynamic level-of-detail (LOD) for label density and customizable positioning.

Parameters:

Name Type Description Default
color Sequence[float]

The RGBA color of the axis lines (0.0 to 1.0).

(1.0, 1.0, 1.0, 1.0)
thickness float

The thickness of the axis lines in pixels.

2.5
label_size int

The base font size used for label layout calculations.

14
label_color Sequence[float]

The RGBA color tint applied to the text labels.

(1, 1, 1, 1)
label_density int

A divisor for the screen dimension to determine target step size. Higher values result in more frequent labels (tighter spacing), while lower values result in fewer labels. Similar to the density logic in Grid.

10
label_offset_x Sequence[float]

A tuple (dx, dy) in pixels indicating the rendering offset for labels along the X-axis. Used to center or position text relative to the tick mark.

(0, -4)
label_offset_y Sequence[float]

A tuple (dx, dy) in pixels indicating the rendering offset for labels along the Y-axis.

(-4, 0)
label_offset_0 Sequence[float]

A tuple (dx, dy) in pixels specifically for the origin (0, 0) label, usually positioned in a quadrant to avoid overlapping both axes.

(-4, -4)
cover_background bool

If True, disables alpha blending for the text pixels. Instead of mixing with the background, the text color strictly overwrites the destination pixels. This is useful for making labels "erase" or cover underlying grid lines to improve legibility.

False
antyaliasing bool

If True, smooths the edges of the axis lines (default True)

True
draw_arrows bool

If True, draws arrows at the ends of the axes (default True)

True
arrow_size float

The size of the arrows in pixels (default 10.0)

25
arrow_style ArrowStyle

The style of the arrows (default ArrowStyle.HARPOON)

HARPOON
arrow_color Sequence[float]

The RGBA color of the arrows (0.0 to 1.0).

(0.8, 0.8, 0.8, 1.0)
Example

Creating a yellow coordinate axis with large labels that cover the elements behind them (such as grid lines):

import FluxRender as fr

# [Initializing the scene and coordinate system]

axis = fr.Axis(
    color = (1, 0.8, 0, 1),  # Yellow axes
    label_size = 18,    # Larger font size for labels
    label_color = (1, 0.8, 0, 1),  # Yellow labels
    cover_background = True,  # Make labels cover elements behind them
    arrow_color = (1, 0.8, 0, 1)  # Yellow arrows
)
scene.add(axis)

Source code in FluxRender/ui.py
def __init__(self,
             color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
             thickness: float = 2.5,
             label_size: int = 14,
             label_color: Sequence[float] = (1, 1, 1, 1),
             label_density: int = 10,
             label_offset_x: Sequence[float] = (0, -4),
             label_offset_y: Sequence[float] = (-4, 0),
             label_offset_0: Sequence[float] = (-4, -4),
             cover_background: bool = False,
             antyaliasing: bool = True,
             draw_arrows: bool = True,
             arrow_size: float = 25,
             arrow_style: ArrowStyle = ArrowStyle.HARPOON,
             arrow_color: Sequence[float] = (0.8, 0.8, 0.8, 1.0)
             ):

    """
    Args:
        color (Sequence[float]): The RGBA color of the axis lines (0.0 to 1.0).
        thickness (float): The thickness of the axis lines in pixels.
        label_size (int): The base font size used for label layout calculations.
        label_color (Sequence[float]): The RGBA color tint applied to the text labels.
        label_density (int): A divisor for the screen dimension to determine target step size.
            Higher values result in more frequent labels (tighter spacing), while lower values
            result in fewer labels. Similar to the density logic in Grid.
        label_offset_x (Sequence[float]): A tuple (dx, dy) in pixels indicating the rendering offset
            for labels along the X-axis. Used to center or position text relative to the tick mark.
        label_offset_y (Sequence[float]): A tuple (dx, dy) in pixels indicating the rendering offset
            for labels along the Y-axis.
        label_offset_0 (Sequence[float]): A tuple (dx, dy) in pixels specifically for the origin (0, 0) label,
            usually positioned in a quadrant to avoid overlapping both axes.
        cover_background (bool): If True, disables alpha blending for the text pixels.
            Instead of mixing with the background, the text color strictly overwrites
            the destination pixels. This is useful for making labels "erase" or cover
            underlying grid lines to improve legibility.
        antyaliasing (bool): If True, smooths the edges of the axis lines (default True)
        draw_arrows (bool): If True, draws arrows at the ends of the axes (default True)
        arrow_size (float): The size of the arrows in pixels (default 10.0)
        arrow_style (ArrowStyle): The style of the arrows (default ArrowStyle.HARPOON)
        arrow_color (Sequence[float]): The RGBA color of the arrows (0.0 to 1.0).

    Example:
        Creating a yellow coordinate axis with large labels that cover the elements behind them (such as grid lines):
        ```python
        import FluxRender as fr

        # [Initializing the scene and coordinate system]

        axis = fr.Axis(
            color = (1, 0.8, 0, 1),  # Yellow axes
            label_size = 18,    # Larger font size for labels
            label_color = (1, 0.8, 0, 1),  # Yellow labels
            cover_background = True,  # Make labels cover elements behind them
            arrow_color = (1, 0.8, 0, 1)  # Yellow arrows
        )
        scene.add(axis)
        ```
    """


    self.color_gpu = ti.Vector.field(4, dtype=float, shape=())
    self.label_color_gpu = ti.Vector.field(4, dtype=float, shape=())

    self.font = FontAtlas(label_size)

    self.color = color
    self.label_color = label_color
    self.thickness = thickness
    self.label_density = label_density

    self.label_offset_x = label_offset_x
    self.label_offset_y = label_offset_y
    self.label_offset_0 = label_offset_0

    self.draw_arrows = draw_arrows
    self.arrow_size = arrow_size
    self.arrow_style = arrow_style
    self.arrow_color = arrow_color

    self.antyaliasing = antyaliasing
    self.cover_background = cover_background

    self.coords = None

    self._max_chars = 400
    self._char_positions = ti.Vector.field(2, dtype=float, shape=self._max_chars)
    self._char_indices = ti.field(dtype=int, shape=self._max_chars)
    self._chars_count = ti.field(dtype=int, shape=())
    self._char_sizes = ti.Vector.field(2, dtype=float, shape=self._max_chars)

    self._np_indices = np.zeros(self._max_chars, dtype=np.int32)
    self._np_positions = np.zeros((self._max_chars, 2), dtype=np.float32)
    self._np_sizes = np.zeros((self._max_chars, 2), dtype=np.float32)

    self._last_cam_state = None

    self.arrow_atlas = ArrowAtlas(style=arrow_style)

FluxRender.ui.Grid

Grid(color=(0.6, 0.6, 0.6, 1), thickness=1, density=10, antyaliasing: bool = True)

Bases: Renderable

Creates a visible grid on the coordinate system that adjusts depending on zoom. Supports custom color, thickness, density, and anti-aliasing.

Parameters:

Name Type Description Default
color tuple / list

The RGBA color of the grid lines. Defaults to (0.6, 0.6, 0.6, 1).

(0.6, 0.6, 0.6, 1)
thickness float

The thickness of the grid lines in pixels. Defaults to 1.

1
density int

The target number of grid lines across the visible range (higher means more lines). Defaults to 10.

10
antyaliasing bool

Whether to apply anti-aliasing. Defaults to True.

True
Example

Creating a double grid with different colors and densities:

import FluxRender as fr

# [Initializing the scene and coordinate system]

# Create a main grid with lower density
main_grid = fr.Grid()

# Create a secondary, less prominent grid with higher density
secondary_grid = fr.Grid(
    color=(0.6, 0.6, 0.6, 0.5), # More transparent
    density=50
)

scene.add(main_grid, secondary_grid)

Source code in FluxRender/ui.py
def __init__(self, color=(0.6, 0.6, 0.6, 1), thickness=1, density = 10, antyaliasing: bool = True):
    """
    Args:
        color (tuple/list, optional): The RGBA color of the grid lines. Defaults to (0.6, 0.6, 0.6, 1).
        thickness (float, optional): The thickness of the grid lines in pixels. Defaults to 1.
        density (int, optional): The target number of grid lines across the visible range (higher means more lines). Defaults to 10.
        antyaliasing (bool, optional): Whether to apply anti-aliasing. Defaults to True.

    Example:
        Creating a double grid with different colors and densities:
        ```python
        import FluxRender as fr

        # [Initializing the scene and coordinate system]

        # Create a main grid with lower density
        main_grid = fr.Grid()

        # Create a secondary, less prominent grid with higher density
        secondary_grid = fr.Grid(
            color=(0.6, 0.6, 0.6, 0.5), # More transparent
            density=50
        )

        scene.add(main_grid, secondary_grid)
        ```
    """

    self.coords = None

    self.color = color
    self.thickness = thickness
    self.density = density
    self.antyaliasing = antyaliasing

FluxRender.ui.UIStyle dataclass

UIStyle(background_color: Optional[Sequence[float]] = None, hover_background_color: Optional[Sequence[float]] = None, active_background_color: Optional[Sequence[float]] = None, text_color: Optional[Sequence[float]] = None, hover_text_color: Optional[Sequence[float]] = None, text_stroke: Optional[int] = None, text_stroke_color: Optional[Sequence[float]] = None, active_text_color: Optional[Sequence[float]] = None, border_radius: Optional[int] = None, font_size: Optional[int] = None, padding: Optional[Tuple[int, int]] = None, display: Optional[bool] = None, visible: Optional[bool] = None)

A highly flexible, CSS-like styling configuration for UI elements.

Unlike rigid styling properties, UIStyle utilizes a cascading resolution system. By default, all attributes are initialized to None. This allows elements to intelligently inherit properties from their parent containers or fall back to their class-specific and global default themes.

The Cascading Resolution Order

When a UI element needs to render, it resolves its style properties in this exact order: 1. Instance Override: Is it explicitly set in this specific UIStyle instance? 2. Parent Inheritance: If the property is inheritable, does the parent container have it set? 3. Class Default: What is the natural default for this element type (e.g., Button vs. Container)? 4. Global Theme: The ultimate fallback theme defined by the engine.

Inheritable vs. Non-Inheritable Properties
  • Inheritable Properties: Typography and deep layout states (e.g., text_color, display). If you set these on a container, all child elements inside will inherit them.
  • Non-Inheritable Properties: Physical bounds and surface appearances (e.g., background_color, padding, visible). Setting a red background on a container will NOT make its inner buttons red.

Parameters:

Name Type Description Default
background_color Optional[Sequence[float]]

The background color of the widget in RGBA format.

None
hover_background_color Optional[Sequence[float]]

The background color when the mouse hovers over the widget.

None
active_background_color Optional[Sequence[float]]

The background color when the widget is active (e.g., clicked).

None
text_color Optional[Sequence[float]]

(Inheritable) The color of the text in RGBA format.

None
hover_text_color Optional[Sequence[float]]

(Inheritable) The color of the text when the mouse hovers.

None
text_stroke Optional[int]

(Inheritable) The width of the stroke around the text.

None
text_stroke_color Optional[Sequence[float]]

(Inheritable) The color of the stroke around the text.

None
active_text_color Optional[Sequence[float]]

(Inheritable) The color of the text when the widget is active.

None
border_radius Optional[int]

Corner rounding radius.

None
font_size Optional[int]

(Inheritable) The size of the font used for the widget's text.

None
padding Optional[Tuple[int, int]]

Inner spacing (x, y) between the widget's border and its internal content.

None
display Optional[bool]

(Inheritable) Toggles rendering for both the element AND all of its children.

None
visible Optional[bool]

Toggles rendering for the element's surface only. Children remain drawn.

None
Example

Creating a button with custom styling:

import FluxRender as fr

# [Initializing the scene and coordinate system]

# Create a container style with a warning aesthetic.
# Background is explicitly red, and text is explicitly yellow.
warning_panel_style = fr.UIStyle(
    background_color = (1.0, 0.0, 0.0, 0.5),
    text_color = (1.0, 1.0, 0.0, 1.0)
)

warning_container = fr.VBox(position=(100, 100), style=warning_panel_style)

# Define function to handle button click
def acknowledge_warning():
    print("Warning acknowledged!")


# Create a button WITHOUT passing any specific style.
# It will use its default button background, but intelligently inherit the yellow text color from the warning container.
action_button = fr.Button(
    text = "Understood",
    on_click = acknowledge_warning
)

warning_container.add(action_button)
scene.add(warning_container)

FluxRender.ui.VBox

VBox(position: Sequence[int] = (20, 200), spacing: int = 15, align: Align = Align.LEFT_TOP, common_width: int = None, common_height: int = None, style: UIStyle = None)

Bases: Container

A vertical layout manager that automatically stacks its children from top to bottom.

The VBox utilizes a deferred layout resolution system. It does not calculate positions immediately upon adding elements. Instead, it waits until the entire UI tree is constructed, and then recursively computes bounding boxes (bottom-up) and element coordinates (top-down). This ensures pixel-perfect alignment regardless of the order in which nested containers and widgets are added.

Parameters:

Name Type Description Default
position Sequence[int]

The (x, y) coordinates for the container's anchor point.

(20, 200)
spacing int

The number of pixels inserted vertically between each child element.

15
align Align

The alignment method that objects in the container will inherit.

LEFT_TOP
common_width int

Forces a uniform width for all DIRECT children (e.g., Buttons) added to this specific container. Does not affect deeply nested elements.

None
common_height int

Forces a uniform height for all DIRECT children added to this specific container. Does not affect deeply nested elements.

None
style UIStyle

The UIStyle object dictating the container's appearance (e.g., background, padding). Inheritable properties provided here will cascade to all children.

None
Example

Creating a vertical set of three buttons:

import FluxRender as fr

# [Inicialize the scene and coordinate system]

# Define the container for the buttons
vertical_container = fr.VBox((30, 400), common_height=40, common_width=290)

# Create a function called by buttons
def print_name(button):
    print(f"Button pressed: {button.text}")

# Create buttons
button1 = fr.Button("Orange", print_name)
button2 = fr.Button("Blue", print_name)
button3 = fr.Button("Green", print_name)

# Add buttons to the container
vertical_container.add(button1, button2, button3)

# Add the container to the scene
scene.add(vertical_container)

Creating a vertical set of three buttons using the context manager to automatically add buttons to the container:

import FluxRender as fr

# [Inicialize the scene and coordinate system]

# Define the container for the buttons
with fr.VBox((30, 400), common_height=40, common_width=290) as vertical_container:

    # Create a function called by buttons
    def print_name(button):
        print(f"Button pressed: {button.text}")

    # Create buttons
    button1 = fr.Button("Orange", print_name)
    button2 = fr.Button("Blue", print_name)
    button3 = fr.Button("Green", print_name)


# Add the container to the scene
scene.add(vertical_container)

Source code in FluxRender/ui.py
def __init__(self,
             position: Sequence[int] = (20, 200),
             spacing: int = 15,
             align: Align = Align.LEFT_TOP,
             common_width: int = None,
             common_height: int = None,
             style: UIStyle = None
             ):
    """
    Args:
        position: The (x, y) coordinates for the container's anchor point.
        spacing: The number of pixels inserted vertically between each child element.
        align: The alignment method that objects in the container will inherit.
        common_width: Forces a uniform width for all DIRECT children (e.g., Buttons)
            added to this specific container. Does not affect deeply nested elements.
        common_height: Forces a uniform height for all DIRECT children added to this
            specific container. Does not affect deeply nested elements.
        style: The UIStyle object dictating the container's appearance (e.g., background,
            padding). Inheritable properties provided here will cascade to all children.

    Example:
        Creating a vertical set of three buttons:
        ```python
        import FluxRender as fr

        # [Inicialize the scene and coordinate system]

        # Define the container for the buttons
        vertical_container = fr.VBox((30, 400), common_height=40, common_width=290)

        # Create a function called by buttons
        def print_name(button):
            print(f"Button pressed: {button.text}")

        # Create buttons
        button1 = fr.Button("Orange", print_name)
        button2 = fr.Button("Blue", print_name)
        button3 = fr.Button("Green", print_name)

        # Add buttons to the container
        vertical_container.add(button1, button2, button3)

        # Add the container to the scene
        scene.add(vertical_container)
        ```

        Creating a vertical set of three buttons using the context manager to automatically add buttons to the container:
        ```python
        import FluxRender as fr

        # [Inicialize the scene and coordinate system]

        # Define the container for the buttons
        with fr.VBox((30, 400), common_height=40, common_width=290) as vertical_container:

            # Create a function called by buttons
            def print_name(button):
                print(f"Button pressed: {button.text}")

            # Create buttons
            button1 = fr.Button("Orange", print_name)
            button2 = fr.Button("Blue", print_name)
            button3 = fr.Button("Green", print_name)


        # Add the container to the scene
        scene.add(vertical_container)
        ```
    """

    super().__init__(position, spacing, align, common_width, common_height, style)

    self._current_y = position[1]

FluxRender.ui.HBox

HBox(position: Sequence[int] = (20, 100), spacing: int = 15, align: Align = Align.LEFT_TOP, common_width: int = None, common_height: int = None, style: UIStyle = None)

Bases: Container

A horizontal layout manager that automatically arranges its children from left to right.

The HBox relies on a robust deferred layout architecture. By separating the hierarchy building phase from the mathematical layout phase, it can dynamically adapt its own bounding box to wrap exactly around its content, making it highly scalable for complex, nested UI structures.

Parameters:

Name Type Description Default
position Sequence[int]

The (x, y) coordinates for the container's anchor point.

(20, 100)
spacing int

The number of pixels inserted horizontally between each child element.

15
align Align

The alignment method that objects in the container will inherit.

LEFT_TOP
common_width int

Forces a uniform width for all DIRECT children (e.g., Buttons) added to this specific container. Does not affect deeply nested elements.

None
common_height int

Forces a uniform height for all DIRECT children added to this

None
spacing int

The number of pixels inserted horizontally between each child element.

15
align Align

The alignment method that objects in the container will inherit.

LEFT_TOP
common_width int

Forces a uniform width for all DIRECT children (e.g., Buttons) added to this specific container. Does not affect deeply nested elements.

None
common_height int

Forces a uniform height for all DIRECT children added to this specific container. Does not affect deeply nested elements.

None
style UIStyle

The UIStyle object dictating the container's appearance (e.g., background, padding). Inheritable properties provided here will cascade to all children.

None
Example

Creating a horizontal set of three buttons:

import FluxRender as fr

# [Inicialize the scene and coordinate system]

# Define the container for the buttons
horizontal_container = fr.HBox((10, 80), common_height=40, common_width=290)

# Create a function called by buttons
def print_name(button):
    print(f"Button pressed: {button.text}")

# Create buttons
button1 = fr.Button("Orange", print_name)
button2 = fr.Button("Blue", print_name)
button3 = fr.Button("Green", print_name)

# Add buttons to the container
horizontal_container.add(button1, button2, button3)

# Add the container to the scene
scene.add(horizontal_container)

Creating a horizontal set of three buttons using the context manager to automatically add buttons to the container:

import FluxRender as fr

# [Inicialize the scene and coordinate system]

# Define the container for the buttons
with fr.HBox((10, 80), common_height=40, common_width=290) as horizontal_container:

    # Create a function called by buttons
    def print_name(button):
        print(f"Button pressed: {button.text}")

    # Create buttons
    button1 = fr.Button("Orange", print_name)
    button2 = fr.Button("Blue", print_name)
    button3 = fr.Button("Green", print_name)

# Add the container to the scene
scene.add(horizontal_container)

Source code in FluxRender/ui.py
def __init__(self,
             position: Sequence[int] = (20, 100),
             spacing: int = 15,
             align: Align = Align.LEFT_TOP,
             common_width: int = None,
             common_height: int = None,
             style: UIStyle = None
             ):
    """
    Args:
        position: The (x, y) coordinates for the container's anchor point.
        spacing: The number of pixels inserted horizontally between each child element.
        align: The alignment method that objects in the container will inherit.
        common_width: Forces a uniform width for all DIRECT children (e.g., Buttons)
            added to this specific container. Does not affect deeply nested elements.
        common_height: Forces a uniform height for all DIRECT children added to this
        spacing: The number of pixels inserted horizontally between each child element.
        align: The alignment method that objects in the container will inherit.
        common_width: Forces a uniform width for all DIRECT children (e.g., Buttons)
            added to this specific container. Does not affect deeply nested elements.
        common_height: Forces a uniform height for all DIRECT children added to this
            specific container. Does not affect deeply nested elements.
        style: The UIStyle object dictating the container's appearance (e.g., background,
            padding). Inheritable properties provided here will cascade to all children.

    Example:
        Creating a horizontal set of three buttons:
        ```python
        import FluxRender as fr

        # [Inicialize the scene and coordinate system]

        # Define the container for the buttons
        horizontal_container = fr.HBox((10, 80), common_height=40, common_width=290)

        # Create a function called by buttons
        def print_name(button):
            print(f"Button pressed: {button.text}")

        # Create buttons
        button1 = fr.Button("Orange", print_name)
        button2 = fr.Button("Blue", print_name)
        button3 = fr.Button("Green", print_name)

        # Add buttons to the container
        horizontal_container.add(button1, button2, button3)

        # Add the container to the scene
        scene.add(horizontal_container)
        ```

        Creating a horizontal set of three buttons using the context manager to automatically add buttons to the container:
        ```python
        import FluxRender as fr

        # [Inicialize the scene and coordinate system]

        # Define the container for the buttons
        with fr.HBox((10, 80), common_height=40, common_width=290) as horizontal_container:

            # Create a function called by buttons
            def print_name(button):
                print(f"Button pressed: {button.text}")

            # Create buttons
            button1 = fr.Button("Orange", print_name)
            button2 = fr.Button("Blue", print_name)
            button3 = fr.Button("Green", print_name)

        # Add the container to the scene
        scene.add(horizontal_container)
        ```
    """

    super().__init__(position, spacing, align, common_width, common_height, style)

    self._current_x = position[0]

FluxRender.colors.ColorMapper

ColorMapper(min_hue: int = 240, max_hue: int = 0, min_saturation: float = 0.4, max_saturation: float = 1.0, min_lightness: float = 0.3, max_lightness: float = 0.65, min_alpha: float = 0.8, max_alpha: float = 1.0, scale_type: ScaleType = ScaleType.LINEAR, scale_function=None, min_value: float = None, max_value: float = None)

A dynamic color interpolation engine that translates scalar fields into vibrant visual gradients.

The ColorMapper acts as the visual translator for the mathematical engine. It takes raw scalar values (such as velocity magnitude, divergence, or custom topological metrics) and smoothly maps them to physical RGBA colors.

Crucially, all color interpolation is performed natively within the HSL (Hue, Saturation, Lightness) color space rather than RGB. This architectural choice guarantees perceptually uniform, vivid transitions and completely eliminates the "muddy" or desaturated mid-tones commonly seen in standard linear color blending. It features highly flexible normalization, allowing bounds to be strictly locked at initialization or dynamically injected frame-by-frame by the rendering entities (like VectorField or ParticleSystem).

Parameters:

Name Type Description Default
min_hue int

Minimum hue in degrees (0-360). Default is 240 (blue).

240
max_hue int

Maximum hue in degrees (0-360). Default is 0 (red).

0
min_saturation float

Minimum saturation (0-1). Default is 0.4.

0.4
max_saturation float

Maximum saturation (0-1). Default is 1.0.

1.0
min_lightness float

Minimum lightness (0-1). Default is 0.3.

0.3
max_lightness float

Maximum lightness (0-1). Default is 0.65.

0.65
min_alpha float

Minimum alpha (0-1). Default is 0.8.

0.8
max_alpha float

Maximum alpha (0-1). Default is 1.0.

1.0
scale_type ScaleType

The type of scaling to apply to the input values. Default is ScaleType.LINEAR.

LINEAR
scale_function Callable

A custom single-argument function f(t). Input t is normalized in [0.0, 1.0], and the output must also be within [0.0, 1.0]. Used only if scale_type is ScaleType.CUSTOM.

None
min_value float

Hardcoded minimum value for normalization. If None, the rendering entity will dynamically calculate and inject the minimum value of the current frame.

None
max_value float

Hardcoded maximum value for normalization. If None, the rendering entity will dynamically calculate and inject the maximum value.

None
Example

Creating a highly saturated thermal gradient (Blue to Red) with a custom logarithmic-like curve:

import FluxRender as fr

# [Initialize scene and coordinate system here]

# This mapper transitions from green to orange, but uses a square-root curve
mapper = fr.ColorMapper(
    min_hue=150,
    max_hue=20,
    min_saturation=0.8,
    max_saturation=1,
    min_lightness=0.1,
    max_lightness=0.6,
    min_alpha=1,
    scale_type = fr.ScaleType.CUSTOM,
    scale_function = lambda x: x ** 0.5,
)

# Apply the mapper directly to a ParticleSystem
particles = fr.ParticleSystem(
    vec_function = lambda x, y: (np.sin(x), np.cos(x) * np.sin(y)),
    color_mapper = mapper,
)

scene.add(particles)

Source code in FluxRender/colors.py
def __init__(self,
             min_hue: int = 240, max_hue: int = 0,
             min_saturation: float = 0.4, max_saturation: float = 1.0,
             min_lightness: float = 0.3, max_lightness: float = 0.65,
             min_alpha: float = 0.8, max_alpha: float = 1.0,
             scale_type: ScaleType = ScaleType.LINEAR,
             scale_function = None,
             min_value: float = None, max_value: float = None
             ):
    """
    Args:
        min_hue (int): Minimum hue in degrees (0-360). Default is 240 (blue).
        max_hue (int): Maximum hue in degrees (0-360). Default is 0 (red).
        min_saturation (float): Minimum saturation (0-1). Default is 0.4.
        max_saturation (float): Maximum saturation (0-1). Default is 1.0.
        min_lightness (float): Minimum lightness (0-1). Default is 0.3.
        max_lightness (float): Maximum lightness (0-1). Default is 0.65.
        min_alpha (float): Minimum alpha (0-1). Default is 0.8.
        max_alpha (float): Maximum alpha (0-1). Default is 1.0.
        scale_type (ScaleType): The type of scaling to apply to the input values. Default is ScaleType.LINEAR.
        scale_function (Callable, optional): A custom single-argument function f(t). Input t is normalized in [0.0, 1.0], and the output must also be within [0.0, 1.0]. Used only if `scale_type` is `ScaleType.CUSTOM`.
        min_value (float, optional): Hardcoded minimum value for normalization. If None, the rendering entity will dynamically calculate and inject the minimum value of the current frame.
        max_value (float, optional): Hardcoded maximum value for normalization. If None, the rendering entity will dynamically calculate and inject the maximum value.

    Example:
        Creating a highly saturated thermal gradient (Blue to Red) with a custom logarithmic-like curve:
        ```python
        import FluxRender as fr

        # [Initialize scene and coordinate system here]

        # This mapper transitions from green to orange, but uses a square-root curve
        mapper = fr.ColorMapper(
            min_hue=150,
            max_hue=20,
            min_saturation=0.8,
            max_saturation=1,
            min_lightness=0.1,
            max_lightness=0.6,
            min_alpha=1,
            scale_type = fr.ScaleType.CUSTOM,
            scale_function = lambda x: x ** 0.5,
        )

        # Apply the mapper directly to a ParticleSystem
        particles = fr.ParticleSystem(
            vec_function = lambda x, y: (np.sin(x), np.cos(x) * np.sin(y)),
            color_mapper = mapper,
        )

        scene.add(particles)
        ```
    """

    self.min_hue = min_hue
    self.max_hue = max_hue
    self.min_saturation = min_saturation
    self.max_saturation = max_saturation
    self.min_lightness = min_lightness
    self.max_lightness = max_lightness
    self.min_alpha = min_alpha
    self.max_alpha = max_alpha

    self.min_value = min_value
    self.max_value = max_value

    self.scale_type = scale_type
    self.scale_function = scale_function

calc_color

calc_color(value: float, min_value: float = None, max_value: float = None)

Maps a scalar value to an RGBA color using HSL color space.

Parameters:

Name Type Description Default
value float

The scalar value to map.

required
min_value float

The minimum value of the range (if self.min_value is not None, it will be used).

None
max_value float

The maximum value of the range (if self.max_value is not None, it will be used).

None

Returns:

Name Type Description
color tuple

A tuple representing the RGBA color (r, g, b, a).

Source code in FluxRender/colors.py
def calc_color(self, value: float, min_value: float = None, max_value: float = None):
    """
    Maps a scalar value to an RGBA color using HSL color space.

    Args:
        value (float): The scalar value to map.
        min_value (float): The minimum value of the range (if self.min_value is not None, it will be used).
        max_value (float): The maximum value of the range (if self.max_value is not None, it will be used).

    Returns:
        color (tuple): A tuple representing the RGBA color (r, g, b, a).
    """

    # Use instance min_value and max_value if not provided
    if self.min_value is not None and self.max_value is not None:
        min_value = self.min_value
        max_value = self.max_value

    # Normalize value to [0, 1]
    t = (value - min_value) / (max_value - min_value)
    t = np.clip(t, 0.0, 1.0)

    # Interpolate HSL components
    if self.scale_type == ScaleType.LOGARITHMIC:
        t = np.log(1 + 9 * t) / np.log(10)
    elif self.scale_type == ScaleType.EXPONENTIAL:
        t = (10 ** t - 1) / 9
    elif self.scale_type == ScaleType.CUSTOM and self.scale_function is not None:
        t = self.scale_function(t)

    hue = self.min_hue + t * (self.max_hue - self.min_hue)
    saturation = self.min_saturation + t * (self.max_saturation - self.min_saturation)
    lightness = self.min_lightness + t * (self.max_lightness - self.min_lightness)
    alpha = self.min_alpha + t * (self.max_alpha - self.min_alpha)


    return hsl_to_rgba(hue / 360.0, saturation, lightness, alpha)

map_array

map_array(values: ndarray, min_value: float = None, max_value: float = None)

Maps a NumPy array of scalar values to an array of RGBA colors.

Parameters:

Name Type Description Default
values ndarray

A NumPy array of scalar values.

required
min_value float

The minimum value of the range.

None
max_value float

The maximum value of the range.

None

Returns:

Name Type Description
colors ndarray

A NumPy array of RGBA colors corresponding to the input values.

Source code in FluxRender/colors.py
def map_array(self, values: np.ndarray, min_value: float = None, max_value: float = None):
    """
    Maps a NumPy array of scalar values to an array of RGBA colors.

    Args:
        values (np.ndarray): A NumPy array of scalar values.
        min_value (float): The minimum value of the range.
        max_value (float): The maximum value of the range.

    Returns:
        colors (np.ndarray): A NumPy array of RGBA colors corresponding to the input values.
    """

    diff = max_value - min_value
    if diff == 0: diff = 1.0 # Avoid division by zero when all values are the same

    with np.errstate(divide='ignore', over='ignore', invalid='ignore'):
        # Normalize value to [0, 1]
        t = (values - min_value) / diff
        t = np.clip(t, 0.0, 1.0)

        if self.scale_type == ScaleType.LOGARITHMIC:
            t = np.log10(1 + 9 * t)
        elif self.scale_type == ScaleType.EXPONENTIAL:
            t = (np.power(10.0, t) - 1) / 9.0
        elif self.scale_type == ScaleType.CUSTOM and self.scale_function is not None:
            try:
                # The function can take list/array inputs and return list/array outputs (vectorized)
                t = self.scale_function(t)
            except Exception:
                # The function is a regular Python function (for each point individually)
                f = np.vectorize(self.scale_function)
                t = f(t)

        hue = self.min_hue + t * (self.max_hue - self.min_hue)
        saturation = self.min_saturation + t * (self.max_saturation - self.min_saturation)
        lightness = self.min_lightness + t * (self.max_lightness - self.min_lightness)
        alpha = self.min_alpha + t * (self.max_alpha - self.min_alpha)

        r, g, b = hsl_to_rgb_vectorized(hue, saturation, lightness)

        return np.column_stack((r, g, b, alpha)).astype(np.float32)

FluxRender.ui.create_mode_switch

create_mode_switch(scene: Scene, vector_field: VectorField, add_to_scene: bool = True) -> Button

Creates an interactive UI button that cycles through the available rendering modes of a vector field.

This factory function generates a state-machine toggle switch. Each click smoothly advances the target VectorField to its next sequential FieldMode (e.g., from WORLD_FIXED to SCREEN_FIXED) and automatically updates the button's text label to reflect the current state. When the final mode is reached, the switch loops seamlessly back to the first available mode.

Parameters:

Name Type Description Default
scene Scene

The main scene object where the button will be registered and rendered.

required
vector_field VectorField

The target vector field whose rendering mode will be controlled by this button.

required
add_to_scene bool

If True, the constructed button will be automatically added to the scene.

True

Returns:

Name Type Description
Button Button

The constructed UI button widget, fully bound to the scene and ready for interaction.

Example
import FluxRender as fr
import numpy as np

scene = fr.create_workspace()

def swirling_vortex(x, y):
    vector_dx = np.sin(y) * x
    vector_dy = np.cos(x) * y
    return vector_dx, vector_dy

vector_field = fr.VectorField(swirling_vortex)

fr.create_mode_switch(scene, vector_field)

scene.add(vector_field)
scene.run()
Source code in FluxRender/ui.py
def create_mode_switch(scene: cr.Scene, vector_field: en.VectorField, add_to_scene: bool = True) -> Button:
    """
    Creates an interactive UI button that cycles through the available rendering modes
    of a vector field.

    This factory function generates a state-machine toggle switch. Each click smoothly
    advances the target VectorField to its next sequential `FieldMode` (e.g., from
    WORLD_FIXED to SCREEN_FIXED) and automatically updates the button's text label
    to reflect the current state. When the final mode is reached, the switch loops
    seamlessly back to the first available mode.

    Args:
        scene (cr.Scene): The main scene object where the button will be registered
            and rendered.
        vector_field (en.VectorField): The target vector field whose rendering mode
            will be controlled by this button.
        add_to_scene (bool): If True, the constructed button will be automatically added to the scene.

    Returns:
        Button: The constructed UI button widget, fully bound to the scene and
            ready for interaction.

    Example:
        ```python
        import FluxRender as fr
        import numpy as np

        scene = fr.create_workspace()

        def swirling_vortex(x, y):
            vector_dx = np.sin(y) * x
            vector_dy = np.cos(x) * y
            return vector_dx, vector_dy

        vector_field = fr.VectorField(swirling_vortex)

        fr.create_mode_switch(scene, vector_field)

        scene.add(vector_field)
        scene.run()
        ```
    """

    def toggle_mode(button):
        mode_iter = iter(mode_mapping.keys())
        for mode in mode_iter:
            if mode == vector_field.mode:
                break
        try:
            next_mode = next(mode_iter)
        except StopIteration:
            next_mode = next(iter(mode_mapping.keys()))  # Loop back to the first mode

        vector_field.change_mode(next_mode)
        button.change_text(mode_mapping.get(vector_field.mode, "Unknown Mode"))

    mode_mapping = {
        FieldMode.WORLD_FIXED: "World Fixed",
        FieldMode.SCREEN_FIXED: "Screen Fixed",
        FieldMode.ZOOM_ADAPTIVE: "Zoom Adaptive",
        FieldMode.WORLD_DENSITY_ADAPTIVE: "World Density Adaptive",
        FieldMode.ZOOM_DENSITY_ADAPTIVE: "Zoom Density Adaptive"
    }

    btn = Button(
        text=mode_mapping.get(vector_field.mode, "Unknown Mode"),
        on_click=toggle_mode,
        position=(10, scene.height - 10),
        width=230,
        height=35,
        style=UIStyle(
            font_size=16,
        )
    )

    if add_to_scene:
        scene.add(btn)
    return btn

FluxRender.ui.create_property_switch

create_property_switch(scene: Scene, *target_entities, add_to_scene: bool = True) -> VBox

Creates a vertical UI panel containing a set of buttons to toggle the active rendering property for multiple vector fields or particle systems.

This function generates a data-driven switch menu. When a button is clicked, it updates the 'color_property' attribute of all provided target entities and visually highlights the currently active button while resetting the others.

Smart Custom Button Injection: The menu dynamically evaluates the capabilities of the provided entities before construction. The button for Property.CUSTOM is strictly injected into the UI only if ALL provided target_entities safely support it. This means every entity must either explicitly have a valid custom_color_function defined, or its underlying VectorMathEngine must possess a valid custom_function. This robust check completely eliminates the risk of runtime crashes caused by unsupported custom property switches.

Parameters:

Name Type Description Default
scene Scene

The main scene object where the UI container will be registered.

required
*target_entities

An arbitrary number of objects (e.g., VectorField, ParticleSystem) that possess a 'color_property' attribute to be updated.

()
add_to_scene bool

If True, the constructed VBox will be automatically added to the scene.

True

Returns:

Name Type Description
VBox VBox

The constructed vertical container holding the property buttons, fully bound to the scene.

Example
import FluxRender as fr
import numpy as np

scene = fr.create_workspace()

def swirling_vortex(x, y):
    vector_dx = np.sin(y) * x
    vector_dy = np.cos(x) * y
    return vector_dx, vector_dy

vector_field = fr.VectorField(swirling_vortex)
particles = fr.ParticleSystem(swirling_vortex)

fr.create_property_switch(scene, vector_field, particles)

scene.add(vector_field, particles)
scene.run()
Source code in FluxRender/ui.py
def create_property_switch(scene: cr.Scene, *target_entities, add_to_scene: bool = True) -> VBox:
    """
    Creates a vertical UI panel containing a set of buttons to toggle the active rendering property
    for multiple vector fields or particle systems.

    This function generates a data-driven switch menu. When a button is clicked, it updates the
    'color_property' attribute of all provided target entities and visually highlights the currently
    active button while resetting the others.

    Smart Custom Button Injection:
    The menu dynamically evaluates the capabilities of the provided entities before construction.
    The button for `Property.CUSTOM` is strictly injected into the UI only if ALL provided
    `target_entities` safely support it. This means every entity must either explicitly have a valid
    `custom_color_function` defined, or its underlying `VectorMathEngine` must possess a valid
    `custom_function`. This robust check completely eliminates the risk of runtime crashes caused
    by unsupported custom property switches.

    Args:
        scene (cr.Scene): The main scene object where the UI container will be registered.
        *target_entities: An arbitrary number of objects (e.g., VectorField, ParticleSystem)
            that possess a 'color_property' attribute to be updated.
        add_to_scene (bool): If True, the constructed VBox will be automatically added to the scene.

    Returns:
        VBox: The constructed vertical container holding the property buttons, fully bound to the scene.

    Example:
        ```python
        import FluxRender as fr
        import numpy as np

        scene = fr.create_workspace()

        def swirling_vortex(x, y):
            vector_dx = np.sin(y) * x
            vector_dy = np.cos(x) * y
            return vector_dx, vector_dy

        vector_field = fr.VectorField(swirling_vortex)
        particles = fr.ParticleSystem(swirling_vortex)

        fr.create_property_switch(scene, vector_field, particles)

        scene.add(vector_field, particles)
        scene.run()
        ```
    """

        # Data-driven mapping: (Button Label, Target Enum Property)
    property_mapping = [
        ("Component X", Property.COMPONENT_X),
        ("Component Y", Property.COMPONENT_Y),
        ("Velocity", Property.VELOCITY),
        ("Angle", Property.ANGLE),
        ("Divergence", Property.DIVERGENCE),
        ("Curl", Property.CURL),
        ("Jacobian", Property.JACOBIAN),
        ("Okubo-Weiss", Property.OKUBO_WEISS),
        ("Convective Acceleration", Property.CONVECTIVE_ACCELERATION),
    ]

    is_custom_property_available = True
    for entity in target_entities:
        if (not hasattr(entity, 'math_engine') or getattr(entity.math_engine, 'custom_function', None) is None) and \
           (not hasattr(entity, 'custom_color_function') or getattr(entity, 'custom_color_function', None) is None):
            is_custom_property_available = False
            break

    if is_custom_property_available:
        property_mapping.append(("Custom", Property.CUSTOM))

    # region Define styles for active and inactive states [blue]
    active_style = UIStyle(
        background_color=(0.027, 0.212, 0.439, 0.878),
        hover_background_color=(0.027, 0.212, 0.439, 0.878)
    )
    inactive_style = UIStyle(
        background_color=(0.0, 0.5, 1.0, 0.45),
        hover_background_color=(0.0, 0.6, 1.0, 0.55),
    )
    active_custom_style=UIStyle(
            background_color=(0.431, 0.031, 0.431, 0.878),
            hover_background_color=(0.431, 0.031, 0.431, 0.878),
            active_background_color=(0.8, 0.2, 0.8, 0.55)
    )
    inactive_custom_style=UIStyle(
            background_color=(0.8, 0.2, 0.8, 0.45),
            hover_background_color=(0.8, 0.2, 0.8, 0.55),
            active_background_color=(0.431, 0.031, 0.431, 0.878)
    )
    #endregion


    # Internal callback for handling state changes
    def toggle_property(clicked_button):
        for entity in target_entities:
            setattr(entity, 'color_property', clicked_button.property)

        for current_button in generated_buttons:
            is_custom_property = (current_button.property == Property.CUSTOM)

            if current_button == clicked_button:
                current_button.style = active_custom_style if is_custom_property else active_style
            else:
                current_button.style = inactive_custom_style if is_custom_property else inactive_style

    generated_buttons = []

    # Dynamically generate buttons based on the mapping above
    for label_text, target_property in property_mapping:
        is_custom_property = (target_property == Property.CUSTOM)
        is_active = all(getattr(entity, 'color_property', None) == target_property for entity in target_entities)

        initial_style = inactive_style
        if is_active:
            initial_style = active_custom_style if is_custom_property else active_style

        new_button = Button(label_text, toggle_property, style=initial_style)
        new_button.property = target_property
        generated_buttons.append(new_button)

    container = VBox(
        position=(10, scene.height - 10),
        spacing=12,
        common_width=230,
        common_height=35,
        style=UIStyle(
            font_size=16,
            padding=(12, 12),
        )
    )

    container.add(*generated_buttons)
    if add_to_scene:
        scene.add(container)

    return container

FluxRender.ui.create_color_scale_switch

create_color_scale_switch(scene: Scene, mapper: ColorMapper, add_to_scene: bool = True) -> Button

Creates an interactive UI button that cycles through the available color scaling types.

This switch is state-aware. It dynamically builds its cycle path based on the target ColorMapper's capabilities. If the user has not defined a custom scaling function, the switch safely skips the CUSTOM state to prevent runtime errors, cycling only through standard mathematical scales.

Parameters:

Name Type Description Default
scene Scene

The main scene object where the button will be registered.

required
mapper ColorMapper

The target color mapper whose scale type will be controlled.

required
add_to_scene bool

If True, the constructed Button will be automatically added to the scene.

True

Returns:

Name Type Description
Button Button

The constructed UI button widget.

Example
import FluxRender as fr
import numpy as np

scene = fr.create_workspace()

def swirling_vortex(x, y):
    vector_dx = np.sin(y) * x
    vector_dy = np.cos(x) * y
    return vector_dx, vector_dy

color_mapper = fr.ColorMapper()
vector_field = fr.VectorField(swirling_vortex, color_mapper=color_mapper)

fr.create_color_scale_switch(scene, color_mapper)

scene.add(vector_field)
scene.run()
Source code in FluxRender/ui.py
def create_color_scale_switch(scene: cr.Scene, mapper: en.ColorMapper, add_to_scene: bool = True) -> Button:
    """
    Creates an interactive UI button that cycles through the available color scaling types.

    This switch is state-aware. It dynamically builds its cycle path based on the target
    ColorMapper's capabilities. If the user has not defined a custom scaling function,
    the switch safely skips the CUSTOM state to prevent runtime errors, cycling only
    through standard mathematical scales.

    Args:
        scene (cr.Scene): The main scene object where the button will be registered.
        mapper (en.ColorMapper): The target color mapper whose scale type will be controlled.
        add_to_scene (bool): If True, the constructed Button will be automatically added to the scene.

    Returns:
        Button: The constructed UI button widget.

    Example:
        ```python
        import FluxRender as fr
        import numpy as np

        scene = fr.create_workspace()

        def swirling_vortex(x, y):
            vector_dx = np.sin(y) * x
            vector_dy = np.cos(x) * y
            return vector_dx, vector_dy

        color_mapper = fr.ColorMapper()
        vector_field = fr.VectorField(swirling_vortex, color_mapper=color_mapper)

        fr.create_color_scale_switch(scene, color_mapper)

        scene.add(vector_field)
        scene.run()
        ```
    """

    scale_mapping = {
        ScaleType.LINEAR: "Linear",
        ScaleType.LOGARITHMIC: "Logarithmic",
        ScaleType.EXPONENTIAL: "Exponential"
    }

    # SAFETY CHECK: Only inject the CUSTOM mode into the cycle if it's safe to use!
    if hasattr(mapper, 'scale_function') and mapper.scale_function is not None:
        scale_mapping[ScaleType.CUSTOM] = "Custom"

    def toggle_scale(button):
        scale_iter = iter(scale_mapping.keys())
        for scale in scale_iter:
            if scale == mapper.scale_type:
                break
        try:
            next_scale = next(scale_iter)
        except StopIteration:
            next_scale = next(iter(scale_mapping.keys()))  # Loop back to the first scale

        mapper.scale_type = next_scale
        button.change_text(scale_mapping.get(mapper.scale_type, "Unknown Scale"))
        scene._update_all_objects()

    initial_text = scale_mapping.get(mapper.scale_type, "Unknown Scale")

    btn = Button(
        text=initial_text,
        on_click=toggle_scale,
        position=(10, scene.height - 10),
        width=230,
        height=35,
        style=UIStyle(font_size=16)
    )

    if add_to_scene:
        scene.add(btn)
    return btn

FluxRender.ui.create_cursor_probe_display

create_cursor_probe_display(scene: Scene, vector_function: Callable | VectorMathEngine, target_property: Property | None = None, display_position: Sequence[int] = (20, 100), add_to_scene: bool = True) -> Tuple[rg.CursorRegion, pr.DataProbe, DynamicText]

The text widget automatically updates every frame, displaying the value of the requested mathematical property at the current mouse position.

Creates a complete, linked data inspection tool consisting of a cursor tracking region, a mathematical data probe, and a dynamic user interface text display.

Parameters:

Name Type Description Default
scene Scene

The main scene to which the elements will be attached.

required
vector_function Callable | VectorMathEngine

The mathematical function or math engine used for calculations.

required
target_property Property | None

The specific vector field property to measure (e.g., DIVERGENCE). If None, the current value of the vector field will be displayed. Default is None.

None
display_position Sequence[int]

The screen coordinates (x, y) where the UI text will be anchored.

(20, 100)
add_to_scene bool

Whether to automatically add the created elements to the scene. Set to False if you want to manage their addition manually.

True

Returns:

Name Type Description
cursor_tracking_region CursorRegion

The cursor tracking region.

data_probe DataProbe

The data probe.

dynamic_text_display DynamicText

The dynamic text display.

Example
import FluxRender as fr
import numpy as np

scene = fr.create_workspace()

def swirling_vortex(x, y):
    vector_dx = np.sin(y) * x
    vector_dy = np.cos(x) * y
    return vector_dx, vector_dy

color_mapper = fr.ColorMapper()
vector_field = fr.VectorField(swirling_vortex, color_mapper=color_mapper)

# Create dynamic text that displays the current value of a vector function under the cursor
fr.create_cursor_probe_display(scene, swirling_vortex)

scene.add(vector_field)
scene.run()
Source code in FluxRender/ui.py
def create_cursor_probe_display(scene: cr.Scene,
                                vector_function: Callable | me.VectorMathEngine,
                                target_property: en.Property | None = None,
                                display_position: Sequence[int] = (20, 100),
                                add_to_scene: bool = True
                                ) -> Tuple[rg.CursorRegion, pr.DataProbe, DynamicText]:
    """
    The text widget automatically updates every frame, displaying the value of the requested
    mathematical property at the current mouse position.

    Creates a complete, linked data inspection tool consisting of a cursor tracking region,
    a mathematical data probe, and a dynamic user interface text display.

    Args:
        scene (Scene): The main scene to which the elements will be attached.
        vector_function (Callable | VectorMathEngine): The mathematical function or math engine used for calculations.
        target_property (Property | None): The specific vector field property to measure (e.g., DIVERGENCE). If None, the current value of the vector field will be displayed. Default is None.
        display_position (Sequence[int]): The screen coordinates (x, y) where the UI text will be anchored.
        add_to_scene (bool): Whether to automatically add the created elements to the scene. Set to False if you want to manage their addition manually.

    Returns:
        cursor_tracking_region (CursorRegion): The cursor tracking region.
        data_probe (DataProbe): The data probe.
        dynamic_text_display (DynamicText): The dynamic text display.

    Example:
        ```python
        import FluxRender as fr
        import numpy as np

        scene = fr.create_workspace()

        def swirling_vortex(x, y):
            vector_dx = np.sin(y) * x
            vector_dy = np.cos(x) * y
            return vector_dx, vector_dy

        color_mapper = fr.ColorMapper()
        vector_field = fr.VectorField(swirling_vortex, color_mapper=color_mapper)

        # Create dynamic text that displays the current value of a vector function under the cursor
        fr.create_cursor_probe_display(scene, swirling_vortex)

        scene.add(vector_field)
        scene.run()
        ```
    """

    if callable(vector_function):
        vector_function = me.VectorMathEngine(scene, vector_function)

    # Initialize the interactive region tracking the mouse cursor
    cursor_tracking_region = rg.CursorRegion(always_active=True)

    data_probe = pr.DataProbe(cursor_tracking_region, vector_function, target_property)

    def text_provider_function() -> str:
        current_mathematical_value = data_probe.value

        # Format the output string gracefully depending on whether the result is a scalar or a vector
        if target_property is not None:
            formatted_name = target_property.name.replace("_", " ").title()

        if isinstance(current_mathematical_value, (tuple, list)) or hasattr(current_mathematical_value, '__iter__'):
            return f"Value: ({current_mathematical_value[0]:.3f}, {current_mathematical_value[1]:.3f})"
        else:
            return f"{formatted_name}: {current_mathematical_value:.3f}"

    dynamic_text_display = DynamicText(
        position=display_position,
        text=text_provider_function,
    )

    if add_to_scene:
        scene.add(cursor_tracking_region, data_probe, dynamic_text_display)

    return cursor_tracking_region, data_probe, dynamic_text_display