Qt Widgets for NGL Types
ncca.ngl.widgets provides ready-made PySide6 widgets for editing and
displaying NGL math types in a GUI — spin-box clusters for vectors,
colour pickers that speak Vec3/Vec4, and composite editors that emit
Mat4 transforms. They are ordinary QWidgets (each is a QFrame), so
they drop into any Qt layout.
See them all at once with the bundled demo:
uv run python -m ncca.ngl.widgets
The widgets
| Widget | Edits | Value signal |
|---|---|---|
Vec2Widget |
two spin boxes | valueChanged(Vec2) |
Vec3Widget |
three spin boxes | valueChanged(Vec3) |
Vec4Widget |
four spin boxes | valueChanged(Vec4) |
Mat2Widget |
2x2 grid + reset buttons | valueChanged(Mat2) |
Mat3Widget |
3x3 grid + reset buttons + method combo | valueChanged(Mat3) |
Mat4Widget |
4x4 grid + reset buttons + method combo | valueChanged(Mat4) |
TransformWidget |
position / rotation / scale | valueChanged(Mat4) |
LookAtWidget |
eye / look / up | valueChanged(Mat4) (a look_at view matrix) |
RGBColourWidget |
RGB sliders + colour dialog | colourChanged(Vec3) |
RGBAColourWidget |
RGBA sliders + colour dialog | colourChanged(Vec4) |
The vector, transform, look-at and colour widgets share the constructor
shape Widget(parent=None, name="", <initial value...>) — name is the
label shown on the widget frame:
from ncca.ngl import Vec3
from ncca.ngl.widgets import Vec3Widget
position = Vec3Widget(self, "Position", Vec3(0, 1, 0))
The matrix widgets (below) instead take Widget(parent=None, name="",
read_only=False) — no initial-value argument, since they always start
from an identity matrix; use set_value() to set a starting value.
Signals
Beyond the whole-value signal, the vector widgets emit per-component
float signals (xValueChanged, yValueChanged, zValueChanged,
wValueChanged), and the colour widgets per-channel ones
(rValueChanged, gValueChanged, bValueChanged, aValueChanged).
Connect whichever granularity you need:
position.valueChanged.connect(self.set_position) # receives a Vec3
position.yValueChanged.connect(self.set_height) # receives a float
Values are read and written with get_value() / set_value(...)
(get_eye/set_eye, get_look, get_up on LookAtWidget;
set_colour on the colour widgets).
Ranges and steps
The vector widgets default to a small range, so set what your scene needs — per component or all at once:
position.set_range(-10.0, 10.0) # all components
position.set_y_range(0.0, 5.0) # just Y
position.set_single_step(0.1)
set_range()/set_single_step() work the same way on the matrix grid
widgets below, applied to every cell.
Matrix grid widgets
Mat2Widget/Mat3Widget/Mat4Widget edit a matrix as a grid of spin
boxes, with Identity/Zero/Transpose/Inverse buttons that
reset or transform the current value (Inverse shows "Matrix is singular"
instead of raising if the matrix isn't invertible):
from ncca.ngl.widgets import Mat4Widget
model_matrix = Mat4Widget(self, "Model")
model_matrix.valueChanged.connect(viewport.set_model) # Mat4
Mat3Widget and Mat4Widget also have a method combo box
(rotate_x/rotate_y/rotate_z/scale, plus translate on
Mat4Widget) that sets the matrix from the corresponding classmethod.
Selecting a rotation shows a single angle (degrees) spin box; selecting
scale/translate shows an embedded Vec3Widget instead. Whichever
panel is visible updates the matrix live — there's no separate "Apply"
button.
Read-only (view) mode
Pass read_only=True to get a plain display grid with no reset buttons
and no method combo — the cells can't be typed into, but set_value()
still updates what's shown. Use this to display a matrix that's produced
elsewhere (e.g. a TransformWidget output) without offering a second,
conflicting way to edit it:
transform = TransformWidget(self, "Model")
transform_matrix = Mat4Widget(self, "Model Matrix", read_only=True)
transform.valueChanged.connect(transform_matrix.set_value)
Driving a viewport
The typical use is a control panel beside an OpenGL (or WebGPU) viewport: connect the widget signals to slots that store the value and schedule a repaint.
from ncca.ngl import Mat4, Vec3, Vec4
from ncca.ngl.widgets import LookAtWidget, RGBColourWidget, TransformWidget
class ControlPanel(QWidget):
def __init__(self, viewport):
super().__init__()
layout = QVBoxLayout(self)
camera = LookAtWidget(self, "Camera", eye=Vec3(2, 2, 2))
camera.valueChanged.connect(viewport.set_view) # Mat4
transform = TransformWidget(self, "Model")
transform.valueChanged.connect(viewport.set_model) # Mat4
colour = RGBColourWidget(self, "Colour", 0.8, 0.5, 0.2)
colour.colourChanged.connect(viewport.set_colour) # Vec3
for w in (camera, transform, colour):
layout.addWidget(w)
In the viewport slots, store the value and call self.update(); the next
paintGL picks it up. TransformWidget and LookAtWidget both hand you
a finished Mat4 — the model matrix and the view matrix respectively —
so the paint code just multiplies:
mvp = self.project @ self.view @ self.model
The demo dialog in
src/ncca/ngl/widgets/__main__.py
shows every widget wired to live value labels and is a good copy-paste
source.
Full API: Widgets reference.