Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Fiatlight Documentation

Custom types registration

By calling fiatlight.register_type(DataType, DataTypeWithGui), it is possible to register a custom type with its GUI.

For a given type’s GUI, it is possible to customize many aspects. Basically all the callbacks and options inside AnyDataGuiCallbacks can be customized.

Quick path: register_callbacks / make_simple_gui

For typed pins that don’t need internal state (no zoom/pan cache, no per-instance configuration), you don’t need to subclass AnyDataWithGui. The helpers fl.register_callbacks and fl.make_simple_gui accept the fields of AnyDataGuiCallbacks as explicit keyword-only parameters and produce a registered factory in one call.

Read-only display pin (a Points2D typed value):

import fiatlight as fl
import numpy as np
from imgui_bundle import imgui

Points2D = fl.documented_newtype(
    "Points2D",
    np.ndarray,
    "List of 2D pixel coordinates as an (N, 2) int32 ndarray.",
)


def _present(points: Points2D) -> None:
    imgui.text(f"{int(points.shape[0])} point(s)")


fl.register_callbacks(
    Points2D,
    present=_present,
    present_str=lambda p: f"Points2D[n={int(p.shape[0])}]",
    default=lambda: Points2D(np.empty((0, 2), dtype=np.int32)),
)

Edit-able pin (a MyPoint3D model):

class MyPoint3D:
    x: float; y: float; z: float


def _edit(p: MyPoint3D) -> tuple[bool, MyPoint3D]:
    changed_x, p.x = imgui.drag_float("X", p.x)
    changed_y, p.y = imgui.drag_float("Y", p.y)
    changed_z, p.z = imgui.drag_float("Z", p.z)
    return changed_x or changed_y or changed_z, p


fl.register_callbacks(
    MyPoint3D,
    present_str=lambda p: f"MyPoint3D({p.x:.2f}, {p.y:.2f}, {p.z:.2f})",
    default=lambda: MyPoint3D(),
    edit=_edit,
)

Notes:

  • The kwargs are typed and exhaustive — typos are caught by mypy and the IDE autocompletes valid names.

  • default is a shorter alias for default_value_provider; pass at most one.

  • present_collapsible and edit_collapsible default to False for these helpers (small pins), unlike AnyDataGuiCallbacks which defaults them to True (large widgets).

  • make_simple_gui(...) returns the factory without registering it — useful when you want to wire registration yourself.

For a value type that needs internal state (a presenter cache, fiat-attribute handling, save/load of GUI options) you still want a full subclass — see Example 1 below.

Example 1: a customizable Normal Distribution type

Step 1: Define the Custom Type

First, let’s define a new type called NormalDistribution.

class NormalDistribution:
    mean: float = 0.0
    stddev: float = 1.0

    def __init__(self, mean: float = 0.0, stddev: float = 1.0) -> None:
        self.mean = mean
        self.stddev = stddev

Step 2: Create a Class to Handle the Custom Type

Next, we create a class NormalDistributionWithGui that extends AnyDataWithGui and defines the custom presentation and editing logic for the NormalDistribution type.

It will handle:

  • A custom GUI for editing the NormalDistribution type

  • A custom GUI for presenting the NormalDistribution type (using a cached figure, which is updated when the distribution changes)

  • Serialization and deserialization of the custom type

  • A default value provider

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.figure import Figure

import fiatlight as fl
from imgui_bundle import imgui, imgui_fig
import numpy as np


class NormalDistributionWithGui(fl.AnyDataWithGui[NormalDistribution]):
    # Cached figure for the distribution plot
    figure: Figure | None = None
    # boolean to indicate if the figure image should be refreshed
    shall_refresh_figure_image: bool = True

    def __init__(self) -> None:
        super().__init__(NormalDistribution)

        # Edit and present callbacks
        self.callbacks.edit = self._edit_gui
        self.callbacks.present = self._present_gui
        self.callbacks.present_str = lambda value: f"Normal Distrib: Mean={value.mean:.2f}, StdDev={value.stddev:.2f}"

        # Default value provider
        self.callbacks.default_value_provider = lambda: NormalDistribution()

        # Serialization of the custom type
        # (note it would be automatic if we used a Pydantic model)
        self.callbacks.save_to_dict = lambda value: {"mean": value.mean, "stddev": value.stddev}
        self.callbacks.load_from_dict = lambda data: NormalDistribution(mean=data["mean"], stddev=data["stddev"])

        # Callback for handling changes: we need to subscribe to this event
        # in order to update the self.figure when the distribution changes
        self.callbacks.on_change = self._on_change

    def _on_change(self, value: NormalDistribution) -> None:
        # remember to close the previous figure to avoid memory leaks
        if self.figure is not None:
            plt.close(self.figure)

        # Create the figure
        x = np.linspace(value.mean - 4 * value.stddev, value.mean + 4 * value.stddev, 100)
        y = (1 / (value.stddev * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - value.mean) / value.stddev) ** 2)
        figure = plt.figure(figsize=(4, 3))
        plt.plot(x, y)
        plt.title("Normal Distribution")
        plt.xlabel("x")
        plt.ylabel("Density")
        plt.grid(True)

        # Cache the figure
        self.figure = figure

        # Indicate that the figure image should be refreshed
        self.shall_refresh_figure_image = True

    @staticmethod
    def _edit_gui(value: NormalDistribution) -> tuple[bool, NormalDistribution]:
        # Note: we receive the current value and return a tuple with
        # a boolean indicating if the value was modified
        modified = False
        imgui.text("Edit Normal Distribution:")
        imgui.set_next_item_width(100)
        changed, new_mean = imgui.slider_float("Mean", value.mean, -10.0, 10.0)
        if changed:
            value.mean = new_mean
            modified = True
        imgui.set_next_item_width(100)
        changed, new_stddev = imgui.slider_float("StdDev", value.stddev, 0.1, 10.0)
        if changed:
            value.stddev = new_stddev
            modified = True

        return modified, value

    def _present_gui(self, _value: NormalDistribution) -> None:
        # We do not use the value which was passed as a parameter as we use the cached Figure
        # which was updated in the _on_change callback
        imgui_fig.fig("Normal Distribution", self.figure, refresh_image=self.shall_refresh_figure_image)
        self.shall_refresh = False

Step 3: Register the type

Finally, we register the custom type with its GUI, simply by calling the register_type function.

fl.register_type(NormalDistribution, NormalDistributionWithGui)

From now on, the NormalDistribution type will be associated with the NormalDistributionWithGui GUI: any function that uses NormalDistribution as a parameter or as a return type will automatically have a GUI for editing and presenting the NormalDistribution type.

Step 4: Use the custom type in a function

In this example, our function simply returns the NormalDistribution instance that was passed to it. In the screenshot, you can see the “edit” callback in action in the Param edition section, and the “present” callback in the Output section.

def f(distribution: NormalDistribution) -> NormalDistribution:
    return distribution

fl.run(f, app_name="Normal Distribution")
<PIL.Image.Image image mode=RGB size=574x663>

Example 2: a Length type with imperial units

import fiatlight as fl

# Step 1: Define the custom type for which we want to create a GUI
# ================================================================
# Here, our custom type is a NewType on top of float

# Option 1: using the standard library NewType (fiatlight requires you to add a __doc__ to the NewType, so we need to set it manually)
#from typing import NewType
# Length = NewType("Length", float)
# Length.__doc__ = "A length in imperial units or meters"

# or Option 2: using the helper function documented_newtype, which is more concise and automatically adds the __doc__
Length = fl.documented_newtype("Length", float, "A length in imperial units or meters")


# Step 2: Create a class to handle the custom type
# ================================================
from fiatlight import AnyDataWithGui
from fiatlight.fiat_widgets import fontawesome_6_ctx, icons_fontawesome_6
from typing import NewType, Any, Dict
from imgui_bundle import imgui, hello_imgui, imgui_ctx, ImVec4


# The specific GUI for our custom type
class LengthWithGui(AnyDataWithGui[Length]):
    use_imperial_units: bool = False

    def __init__(self) -> None:
        super().__init__(Length)
        self.callbacks.edit = self._edit  # A custom callback for editing the data
        self.callbacks.present = self._present  # A custom callback for presenting the data
        self.callbacks.present_str = self._present_str  # A custom callback for presenting the data as a short string
        self.callbacks.default_value_provider = lambda: Length(1.0)  # A custom callback for providing a default value
        # custom callback for saving the GUI options (here, we save the imperial units option)
        self.callbacks.save_gui_options_to_json = self._save_gui_options_to_json
        self.callbacks.load_gui_options_from_json = self._load_gui_options_from_json

    def _edit(self, value: Length) -> tuple[bool, Length]:
        _, self.use_imperial_units = imgui.checkbox("Imperial", self.use_imperial_units)

        format = "%.3g m" if not self.use_imperial_units else "%.3g yd"
        value_unit = value * 1.09361 if self.use_imperial_units else value
        imgui.set_next_item_width(hello_imgui.em_size(10))
        changed, new_value_unit = imgui.slider_float(
            "Value", value_unit, 1e-5, 1e11, format, imgui.SliderFlags_.logarithmic.value
        )
        if changed:
            value = Length(new_value_unit / 1.09361 if self.use_imperial_units else new_value_unit)
        return changed, value

    @staticmethod
    def _present_str(value: Length) -> str:
        return f"Length: {value:.2f} m"

    @staticmethod
    def _present(value: Length) -> None:
        with fontawesome_6_ctx():
            yd = int(Length(value * 1.09361))
            inches = int((Length(value * 1.09361 - yd) * 36))
            bananas = int(value / 0.2)
            imgui.text(f"Length: {yd} yd {inches:.0f} in (aka {bananas}")
            imgui.same_line()
            with imgui_ctx.push_style_color(imgui.Col_.text.value, ImVec4(1, 0.5, 0, 1)):
                imgui.text(icons_fontawesome_6.ICON_FA_CARROT)
            imgui.same_line()
            imgui.text(")")

    def _save_gui_options_to_json(self) -> Dict[str, Any]:
        return {"use_imperial_units": self.use_imperial_units}

    def _load_gui_options_from_json(self, json: Dict[str, Any]) -> None:
        self.use_imperial_units = json.get("use_imperial_units", False)


# Step 3: Register the custom type with its GUI
# ==============================================
from fiatlight import register_type

register_type(Length, LengthWithGui)


# Step 4: Use the custom type in a function
# =========================================
# A function that uses our custom type
def circle_perimeter(radius: Length) -> Length:
    return Length(2 * 3.14159 * radius)


# Run the function with the GUI
fiatlight.run(circle_perimeter, app_name="Circle Perimeter in banana units")
<PIL.Image.Image image mode=RGB size=247x210>

How to create a new “fiat kit”

fiat_kit_skeleton

fiatlight.fiat_kits.fiat_skeleton is a starting point for creating new widgets: it is a minimalistic kit that contains the necessary files to create a new widget.

fiat_kit_skeleton
├── __init__.py
├── mydata.py                      # An example data or library that you want to present
├── mydata_presenter.py            # The presenter of the data
|                                  # Also contains a derivate of PossibleCustomAttributes
|                                  # where all the custom attributes are defined
|
└── mydata_with_gui.py             # MyDataWithGui: the widget that will be displayed in the GUI
# (inherits from AnyDataWithGui, implements all the callbacks
#  of AnyDataGuiCallbacks, and uses MyDataPresenter for
# complex data presentation)

See files:

fiat_kit_skeleton in action

fiatlight.fiat_kits.fiat_dataframe it was developed starting from the skeleton. It is a good example on how it can be customized.

fiat_dataframe
├── dataframe_presenter.py                  # The presenter of the data (presentation code)
|                                           # Also contains a derivate of PossibleCustomAttributes
|
├── dataframe_with_gui.py                   # The widget that will be displayed in the GUI
|                                           # (inherits from AnyDataWithGui, implements all the callbacks
|                                          #  of AnyDataGuiCallbacks, and uses DataFramePresenter for
|                                          # complex data presentation)

See files: