What’s New¶
Recent changes — since v0.3.0¶
Custom fonts: Application.register_font / set_default_font¶
Two new methods on Application let the Python side ship
custom font files to the browser and apply a document-wide
default font.
app.register_font('Roboto', '/path/to/Roboto-Regular.ttf')
app.register_font('Roboto', '/path/to/Roboto-Bold.ttf',
weight='bold')
app.set_default_font('Roboto', size=13)
register_font(family, source, *, weight, style)– stores the font bytes in an in-memory registry keyed by a monotonic id; the built-in HTTP server exposes them at/_pgwidgets/font/<id>withCache-Control: immutable. Accepts paths (str/os.PathLike) or rawbytes.set_default_font(family, *, size, weight, style)– writes--pg-default-font-{family,size,weight,style}CSS variables on:rootvia a managed<style>element on the JS side. The base widget stylesheet consumes them with safe fallbacks so apps that never opt in see no change in rendering.
Both methods broadcast to every connected session immediately
and replay the entire registry to every new / reconnecting
session before on_connect / reconstruct runs. That
guarantees a widget that’s reconstructed with
set_font(family, ...) always finds the face already
declared on the JS side, even after a full page reload.
The JS handler normalises descriptive TTF weight names
(thin, light, medium, semibold, extrabold,
black, heavy, …) to the numeric CSS values per the
CSS Fonts spec, so a font registry that knows weights by their
metadata name (e.g. Ginga’s font_asst) can pass weights
straight through.
The matching sync- and async-backend API are identical; the
async variant schedules sends via
asyncio.run_coroutine_threadsafe so register_font is
callable from outside the event loop.
See Synchronous API / Asynchronous API for the full API description.
Flask multi-process example¶
examples/flask-multi-process is a new end-to-end demo of
running pgwidgets-python behind Flask + gunicorn + nginx, with
one OS process per browser session. Highlights:
Applicationacceptsws_sock=and a session can be pre-warmed in the worker process before the browser opens its WebSocket – the worker spawns, runs the user-app’sbuild_uiagainst an empty session, and then waits for the WS handshake.Session-aware routing so a Flask front-end can hand each browser tab off to a dedicated worker by session ID.
Dead-child reaping (
waitpid(WNOHANG)) to avoid zombie worker processes piling up.The user app is a Python class (
app.build_ui(session)), so worker pre-warming, multi-tab fanout, and reconstruction reuse the same entry point.The gunicorn / nginx layer serves the bundled pgwidgets-js static assets from the pip-installed package (no separate jsdelivr fetch needed).
See examples/flask-multi-process/README.md for the full
production stack walkthrough.
file_browser: col_key on row-activated¶
pgwidgets.extras.file_browser matches the JS-side
TreeView/TableView activated callback signature change:
FileBrowser._on_row_activated now receives col_key as
the 4th argument, so handlers that double-click a file can see
which column they hit. Existing handlers (3-arg) keep working.
_resolve_kwargs: skipped-positional kwargs¶
The kwarg resolver used by every generated widget method now
allows a caller to skip a positional argument and supply the
next one by keyword. For example, with a method declared as
set_color(bg=None, fg=None), callers may now write
label.set_color(fg="red") directly instead of
label.set_color(None, "red"). Previously the resolver
would reject the keyword-only form for parameters that
preceded any supplied keyword.
method_types: TreeView colour + cell-selection methods classified as ACTION¶
Internal: the four set_*_color overrides and the new
select_cell / select_cells / clear_cell_selection
methods are now ACTION-typed, so the wrapper layer dispatches
each call straight through to the JS side instead of trying to
collapse them into a single _state slot. Matters for
reconstruction-replay; rarely matters for application code.
Recent changes — since v0.2.3¶
Chunked binary transport (both directions)¶
Session gained _send_binary_chunked(wid, method, args, data,
chunk_size=512KB, shape=None, dtype=None) and a matching
incoming-binary path. Large payloads automatically use the
chunked transport — Image.set_binary_image() (and the
reconstruction replay of binary state) auto-pick chunked above a
1 MiB threshold. Session._handle_file_chunk was rewritten as
_handle_binary_chunk to consume the new binary-chunk
envelope: chunks store by chunk_index (robust to ordering)
and reassemble as bytes.
Pre-1.0 API break: drop-end / FileDialog activated
handlers now receive each file’s data field as raw bytes
(was a "data:<mime>;base64,…" string). Each file dict now
also carries an encoding field (currently "bytes";
reserved for future "base64").
pgwidgets.Buffer for typed-array delivery¶
New pgwidgets.Buffer wraps raw bytes with shape and
dtype metadata. When passed to a binary-aware method, the
bytes ride the chunked transport and the receiver on the JS side
gets a typed array of the right shape (Uint8Array,
Float32Array, …) without the per-method casting that used to
be necessary:
from pgwidgets import Buffer
pixels = ... # 2048 * 2048 * 4 bytes
viewer.load_buffer(
Buffer(pixels, shape=(2048, 2048, 4), dtype="uint8"),
[2048, 2048], cache)
Supported dtypes: uint8 / uint16 / uint32 / int8
/ int16 / int32 / float32 / float64.
Construction validates that len(data) == prod(shape) *
itemsize.
Application(ws_sock=…) for race-free port allocation¶
Application accepts an optional pre-bound TCP socket via
ws_sock=. When provided, Application adopts the socket
directly (hands it to websockets.serve(sock=...)) instead of
binding (host, ws_port) itself, and reads ws_port back
from the socket for logging. The motivation is TOCTOU-free port
allocation: the caller can bind in advance, hand the socket in,
and never release the port between “find” and “use” — no other
process can grab it in the gap. The existing ws_port= API
is unchanged.
Flask multi-process demo¶
New examples/flask-multi-process/ example: one pgwidgets
Application per visitor, each in its own OS process with its
own WebSocket port. Demonstrates the new ws_sock= parameter
(child binds, never releases), idle reaping via a grace timer on
app.on_disconnect that handles the subtle case where
app.on_connect doesn’t fire on session-reconnect (the wrapper
also hooks session.add_connection), and serving pgwidgets-js’s
static assets from the pip-installed package rather than a CDN.
Recent changes — since v0.2.1¶
Reliable get_size() / get_position() on every visual widget¶
The auto-sync layer that backs widget getters has been reworked so
widget.get_size() (and get_position() where supported)
returns the current layout-determined value for any visual widget,
not just ones that opted into the resizable / moveable
options. The binding now installs a passive resize listener on
every visual widget — the value is captured into local state for
the getter, but it is not pushed to other connected browsers or
replayed on reconstruction. Explicit widget.resize(w, h) calls
are still replayed, as is interactive resize on widgets that opted
into active sync.
The user-visible upshot: code that calls image.get_size() on an
Image placed in a flex container now returns the real pixel
size the browser laid the widget out at, instead of None or a
stale value. And widgets like Image with
set_expanding(True, True) no longer get pinned to pixel
dimensions on reconnect (a regression in earlier auto-sync work).
The same guard now applies in both reconstruction paths
(top-level state replay AND _transfer_proxy for factory-created
widgets like ToolBarAction), which fixes a “toolbar items
drift farther apart after each reconnect” bug.
map callback survives reconstruction¶
The Python side now forwards map callbacks during a reconstruct
window instead of suppressing them along with the rest of the
state-replay echoes. map is a one-shot lifecycle event tied to
the widget first becoming visually present; missing it on the
Python side meant a user’s map handler stayed un-fired until the
window happened to be resized. Pairs with the JS-side reliability
work for the same callback.
Sensible defaults for more getters¶
Getters now return useful zero-value defaults before any state has
been set or reported from the browser, instead of None:
get_scroll_position()→(0.0, 0.0)get_scroll_percent()→(0.0, 0.0)(or0.0onScrollBar, which uses a single-axis API)get_thumb_percent()→(0.0, 0.0)(or0.0onScrollBar)get_expanding()→(False, False)get_enabled()→Trueget_state()→Falseget_volume()→1.0(HTMLMediaElement convention: 0.0 muted, 1.0 full).
Configured in STATE_KEY_DEFAULTS / STATE_DEFAULTS in
method_types.py.
Earlier — since v0.1.3¶
Major changes¶
TreeView / TableView: dict-tree model¶
Mirroring the JS-side rewrite, TreeView and TableView now
work with hierarchies of dicts keyed by stable string identifiers.
Paths are arrays of those keys and stay valid no matter how the
visible tree is sorted.
tree = W.TreeView(columns=[
{"label": "Name", "key": "NAME", "type": "string"},
{"label": "Type", "key": "TYPE", "type": "string"},
{"label": "Size", "key": "SIZE", "type": "integer"},
], sortable=True)
tree.set_tree({
"Documents": {
"report.pdf": {"TYPE": "PDF", "SIZE": 2400},
"notes.txt": {"TYPE": "Text", "SIZE": 12},
},
"Pictures": {
"photo.jpg": {"TYPE": "JPEG", "SIZE": 3200},
},
})
Highlights:
New column-key-based API:
set_column_width(col_key),sort_by_column(col_key, ascending),insert_column(column, before=None),delete_column(col_key),set_cell(path, col_key, value),set_column_editable(col_key, tf).New column types:
"string"/"integer"/"float"/"boolean"(renders ✓ when truthy) /"icon".halignfield with sensible per-type defaults.New tree methods:
add_tree(tree, parent=None)– merge a dict-tree under a parent path (preserves selection by path).update_tree(tree)– replace the tree, preserve selection.get_subtree(status='all')– return a connected subset (selected / expanded / collapsed nodes plus their descendants and ancestors), round-trippable throughset_tree.clear_selection()– explicit no-arg reset.
Auto-spanning: a row whose value for a column is missing causes the previous present cell to extend across it. Lets parent rows be terse:
{"NAME": "Documents"}(with the rest of the columns omitted) renders as a single cell across the row.TableView.set_dataaccepts a list of dicts (preferred) or a list of arrays.
See Widget Reference for the full reference.
Window controls (TopLevel) and shade (MDISubWindow)¶
TopLevel gains the same window controls that MDISubWindow
has, plus a “shade” (roll up to title bar) state on both.
New TopLevel options (default False except shadeable
which defaults True):
minimizable– show minimize button. Minimized windows auto-stack along the bottom of the viewport.maximizable– show maximize button. Fills the browser viewport (snapshot at click time).lowerable– show send-to-back button.shadeable– collapse to title bar in place. Available from the right-click context menu and via double-click on the title bar.icon– title-bar icon (URL ordata:URI).
New methods: set_icon(url), toggle_minimize(),
toggle_maximize(), toggle_shade(),
set_window_state(state), get_window_state().
New callback window-state is auto-tracked, so the
minimize/maximize/shade state survives a browser reconnect.
MDIWidget.add_widget accepts shadeable (default True).
Right-click on the title bar of either widget opens a context menu with the applicable actions (Raise, Lower, Shade, Minimize, Maximize, Close). The menu supports both click-release and press-drag-release, like a menubar.
Image: binary-frame protocol¶
New method Image.set_binary_image(data, format='jpeg') sends raw
bytes (bytes / bytearray / memoryview) via a WebSocket
binary frame, avoiding the ~33% base64 overhead of set_image.
Useful for animation/streaming. format is one of "jpeg",
"png", "webp", "gif". The latest frame is stored in
widget state and replayed on reconnect.
Callbacks base class¶
New module pgwidgets.callbacks exposes Callbacks, a small
base class that provides the same callback API (add_callback,
on, make_callback, enable_callback, …) as a real
Widget without the widget machinery. Use it for Python-side
composite/utility classes that need to expose handler registration.
FileBrowser (in pgwidgets.extras) is now a subclass and so
supports both add_callback("activated", ...) and
on("activated", ...) directly.
See Callbacks base class.
Robustness improvements¶
Session._sendand_send_binaryno longer hang when the asyncio loop refuses a coroutine (loop closed mid-call,RecursionErrorfrom a deep callback chain, etc.). The schedule failure is logged, the orphan coroutine is closed, and_sendreturnsNoneso the caller continues.json.dumpserrors in_send/_pushare caught and logged instead of crashing reconstruction with a non-JSON- serialisable widget state.After every
create,Session._next_widis advanced past the JS-sidenext_widso subsequent Python allocations skip any auto-allocated sub-widget IDs (matching the JS-side collision rescue). This fixes “callback fires on the wrong widget” cases where a widget likeTreeViewallocates internalScrollBarwidgets in its constructor.
Other notable changes¶
ColorDialognow exposespopup,set_position,set_modal, and themove/closecallbacks (inherited fromDialogon the JS side). Itspopupis wired through the_dialog_popupcustom method so visibility/position state is tracked for reconstruction.MenuActionactivatedcallback signature simplified. Old:handler(widget, text, checked). New:handler(widget)for non-checkable actions;handler(widget, checked)for checkable ones. This is a breaking change for any handler that took the text arg.Widgetbase class now exposesset_allow_text_selection(tf); browser text selection is off by default for most widgets (form controls and the cell editor inTreeViewalways allow selection).clear()onTreeView/TableViewno longer resets thecolumnsstate (it was popping_state["columns"]even though the JS side preserves columns on clear).FileBrowsermigrated to the new dict-treeTableViewAPI internally and now subclassesCallbacks.FixedLayoutcontainer added (auto-generated from the JS definition). UseW.FixedLayout()andlayout.add_widget(child, x, y)to place children at fixed pixel offsets;remove(child)works as on any other container.