Source code for tadkit.utils.ui
import json
# =====================================================
# 1. Value Sanitization Utility
# =====================================================
[docs]
def sanitize_default(default, min_val, max_val, ptype, closed="both", allow_none=False):
"""Clamp and adjust defaults based on type and bounds."""
def cast(x):
try:
return None if x is None else ptype(x)
except (ValueError, TypeError):
return None
d, lo, hi = map(cast, (default, min_val, max_val))
if allow_none and d is None:
return None
eps = 1e-12 if ptype is float else 1
if d is None:
# no default, fall back to min or type zero
if lo is not None:
return lo + eps if closed in ("right", "neither") else lo
return 0 if ptype is int else 0.0
# Clamp lower bound
if lo is not None:
if closed in ("right", "neither") and d <= lo:
d = lo + eps
elif closed in ("both", "left") and d < lo:
d = lo
# Clamp upper bound
if hi is not None:
if closed in ("left", "neither") and d >= hi:
d = hi - eps
elif closed in ("both", "right") and d > hi:
d = hi
return d
# =====================================================
# 2. Widget Factory
# =====================================================
[docs]
class WidgetFactory:
"""Factory for creating UI widgets or no-UI representations."""
WIDGET_STYLE = {"description_width": "initial"}
def __init__(self, frontend="ipywidgets"):
self.frontend = frontend
self._import_ui_libs()
def _import_ui_libs(self):
"""Lazily import UI libraries."""
if self.frontend == "st":
import streamlit as st
self.st = st
elif self.frontend == "ipywidgets":
import ipywidgets as widgets
self.widgets = widgets
# -------------------------------------------------
# Generic dispatcher for UI elements
# -------------------------------------------------
def _dispatch(self, mapping, **kwargs):
func = mapping.get(self.frontend)
if func is None:
raise ValueError(f"Unsupported frontend: {self.frontend}")
return func(**kwargs)
# -------------------------------------------------
# Numeric input
# -------------------------------------------------
[docs]
def make_numeric(
self,
label,
default,
min_val,
max_val,
ptype,
description,
closed="both",
allow_none=False,
):
"""Create a numeric input widget."""
# Sanitize default
if not (allow_none and default is None):
default = sanitize_default(
default, min_val, max_val, ptype, closed, allow_none
)
# Streamlit
def _st_numeric(**kw):
st = self.st
step = 1 if ptype is int else 0.01
fmt = "%d" if ptype is int else "%.6f"
def cast(x):
return None if x is None else ptype(x)
args = {
"label": label,
"value": cast(default),
"step": cast(step),
"format": fmt,
"help": description,
}
if min_val is not None:
args["min_value"] = cast(min_val)
if max_val is not None:
args["max_value"] = cast(max_val)
return st.number_input(**args)
# ipywidgets
def _ipy_numeric(**kw):
w = self.widgets
cls = w.BoundedIntText if ptype is int else w.BoundedFloatText
return cls(
value=default,
min=min_val if min_val is not None else -1e6,
max=max_val if max_val is not None else 1e6,
description=label,
style=self.WIDGET_STYLE,
tooltip=description,
)
# No UI
def _noui_numeric(**kw):
return {
"type": "number",
"label": label,
"default": default,
"min": min_val,
"max": max_val,
"description": description,
"nullable": allow_none,
}
return self._dispatch(
{
"st": _st_numeric,
"ipywidgets": _ipy_numeric,
"no_ui": _noui_numeric,
}
)
# -------------------------------------------------
# Dropdown
# -------------------------------------------------
[docs]
def make_dropdown(self, label, options, default, description):
"""Create a dropdown input."""
options = options or ["(none)"]
default_val = default if default in options else options[0]
return self._dispatch(
{
"st": lambda **kw: self.st.selectbox(
label, options, index=options.index(default_val), help=description
),
"ipywidgets": lambda **kw: self.widgets.Dropdown(
options=options,
value=default_val,
description=label,
style=self.WIDGET_STYLE,
tooltip=description,
),
"no_ui": lambda **kw: {
"type": "dropdown",
"label": label,
"options": options,
"default": default_val,
"description": description,
},
}
)
# -------------------------------------------------
# Text input
# -------------------------------------------------
[docs]
def make_text(self, label, default, description):
value = str(default or "")
return self._dispatch(
{
"st": lambda **kw: self.st.text_input(
label, value=value, help=description
),
"ipywidgets": lambda **kw: self.widgets.Text(
value=value,
description=label,
style=self.WIDGET_STYLE,
tooltip=description,
),
"no_ui": lambda **kw: {
"type": "text",
"label": label,
"default": default,
"description": description,
},
}
)
# -------------------------------------------------
# Dictionary / JSON input
# -------------------------------------------------
[docs]
def make_dict(self, label, default, description):
json_str = json.dumps(default or {}, indent=2)
return self._dispatch(
{
"st": lambda **kw: self.st.text_area(
label, value=json_str, help=description, height=100
),
"ipywidgets": lambda **kw: self.widgets.Textarea(
value=json_str,
description=label,
layout=self.widgets.Layout(width="100%", height="80px"),
tooltip=description,
),
"no_ui": lambda **kw: {
"type": "dict",
"label": label,
"default": default,
"description": description,
},
}
)