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> with Cache-Control: immutable. Accepts paths (str / os.PathLike) or raw bytes.

  • set_default_font(family, *, size, weight, style) – writes --pg-default-font-{family,size,weight,style} CSS variables on :root via 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:

  • Application accepts ws_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’s build_ui against 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) (or 0.0 on ScrollBar, which uses a single-axis API)

  • get_thumb_percent()(0.0, 0.0) (or 0.0 on ScrollBar)

  • get_expanding()(False, False)

  • get_enabled()True

  • get_state()False

  • get_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". halign field 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 through set_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_data accepts 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 or data: 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._send and _send_binary no longer hang when the asyncio loop refuses a coroutine (loop closed mid-call, RecursionError from a deep callback chain, etc.). The schedule failure is logged, the orphan coroutine is closed, and _send returns None so the caller continues.

  • json.dumps errors in _send / _push are caught and logged instead of crashing reconstruction with a non-JSON- serialisable widget state.

  • After every create, Session._next_wid is advanced past the JS-side next_wid so 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 like TreeView allocates internal ScrollBar widgets in its constructor.

Other notable changes

  • ColorDialog now exposes popup, set_position, set_modal, and the move / close callbacks (inherited from Dialog on the JS side). Its popup is wired through the _dialog_popup custom method so visibility/position state is tracked for reconstruction.

  • MenuAction activated callback 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.

  • Widget base class now exposes set_allow_text_selection(tf); browser text selection is off by default for most widgets (form controls and the cell editor in TreeView always allow selection).

  • clear() on TreeView / TableView no longer resets the columns state (it was popping _state["columns"] even though the JS side preserves columns on clear).

  • FileBrowser migrated to the new dict-tree TableView API internally and now subclasses Callbacks.

  • FixedLayout container added (auto-generated from the JS definition). Use W.FixedLayout() and layout.add_widget(child, x, y) to place children at fixed pixel offsets; remove(child) works as on any other container.