Use NativeWidget when a control has backend-specific behavior. WebView is the executable example in psx.extensions.qt_webengine; it exposes PySide6 QWebEngineView without changing PSX markup, reconciliation, or scheduling.
Import the toolkit inside a helper, not at PSX module import time:
def load_types():
from PySide6.QtWebEngineWidgets import QWebEngineView
return QWebEngineViewThis lets applications without WebEngine import psx normally.
An owned factory is destroyed by PSX. A supplied instance must use borrowed ownership and is never destroyed by PSX. WebView maps url/html updates and Qt signals explicitly.
from psx.core.native import NativeWidget
declaration = NativeWidget(
renderer="pyside6",
factory=load_types(),
update=update_webview,
bind_event=bind_webview_event,
name="QWebEngineView",
)update_webview(widget, changed, removed) must define removal behavior. bind_webview_event(widget, event, slot) must connect one callback and return a disposer that disconnects that exact callback.
WebView turns friendly props into Native signal names and returns native_widget(declaration, ...). It supports url, html, on_load_finished, on_url_changed, key, and ref.
from psx import Column, builtin_component_registry, psx
from psx.extensions import WebView
registry = builtin_component_registry()
registry.register("WebView", WebView)
node = psx(
'<Column><WebView url={url} on_load_finished={ready} /></Column>',
scope={"url": "https://example.test", "ready": print}, registry=registry,
)The registry makes the tag available only where requested. Existing markup and the default registry remain unchanged.
Render a compatible WebView twice with the same key, assert the QWebEngineView instance is unchanged, emit a signal before and after replacing the callback, then unmount and assert the ref is cleared. This is implemented in tests/test_qt_webengine_extension.py.
- PySide6 WebEngine requires
PySide6.QtWebEngineWidgets; use an offscreen Qt platform and Chromium no-sandbox flags in CI. - PyQt5/PyQt6 use the same
NativeWidgetmodel but must import their own binding only in their extension package. - Kivy media/native controls need provider-specific lifecycle code; keep it in an optional extension.
- Tkinter widgets normally need a factory accepting the PSX parent (
takes_parent=True). - Headless adapters are useful for deterministic lifecycle tests even when no real widget exists.
- A renderer mismatch means the declaration's
rendererdoes not match the selected backend. - “No event adapter” means
bind_eventwas omitted or did not handle the declared signal. - Do not pass changing native callbacks directly through a new connection; return a disposer and let
EventSlotupdate the callback. - Never claim a native prop is portable unless every supported backend has an explicit contract and tests.