-
This commit is contained in:
308
scrbl/rktwebviewqt-internals.scrbl
Normal file
308
scrbl/rktwebviewqt-internals.scrbl
Normal file
@@ -0,0 +1,308 @@
|
||||
#lang scribble/manual
|
||||
|
||||
@title{Internal Architecture and Behavior of the Qt–Backed WebView Bridge}
|
||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||
|
||||
@italic{Technical architecture notes for maintainers}
|
||||
|
||||
|
||||
|
||||
@;@abstract{
|
||||
This document explains the internal structure, threading model, command
|
||||
dispatch mechanism, JavaScript bridge, event flow, timing behavior, and
|
||||
lifecycle semantics of a Qt WebEngine–backed webview bridge. It focuses on how
|
||||
a synchronous-looking host API is implemented atop Qt’s single-threaded GUI
|
||||
requirements. Public API signatures and usage examples are intentionally
|
||||
omitted here and should be documented in a separate companion file.
|
||||
@;}
|
||||
|
||||
@section{Design Objectives}
|
||||
|
||||
The implementation provides a compact, synchronous façade over Qt WebEngine
|
||||
while preserving GUI-thread safety, predictable behavior, and a simple, typed
|
||||
boundary to the host.
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{All UI work must execute on the Qt GUI thread; the host may call from any thread.}
|
||||
@item{The host experiences a synchronous API; internally, work is dispatched asynchronously and awaited by pumping the event loop in bounded slices.}
|
||||
@item{A minimal JSON bridge enables page→host messages without exposing Qt types over the boundary.}
|
||||
@item{Window lifecycle, geometry, dialogs, and developer tools behave deterministically and are observable via structured events.}
|
||||
]
|
||||
|
||||
@section{Core Building Blocks}
|
||||
|
||||
@subsection{Command and CommandEvent}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{A @tt{Command} encapsulates a request: @emph{code} (enum), @emph{args} (@tt{QVector<QVariant>}), a @emph{result} (@tt{QVariant}), and flags @tt{done} and @tt{js_result_ok}.}
|
||||
@item{@tt{CommandEvent}: a custom @tt{QEvent} (type @tt{QEvent::User + 1}) that carries a pointer to a @tt{Command} from the caller’s thread to the GUI thread.}
|
||||
@item{The GUI thread handles @tt{CommandEvent} in @tt{customEvent}, dispatching to @tt{processCommand} which performs the UI-side operation, sets the result, and flips @tt{done = true}.}
|
||||
]
|
||||
|
||||
@subsection{@tt{Rktwebview_qt} Controller}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{Owns the singleton @tt{QApplication} and initializes the event-pump timers.}
|
||||
@item{Allocates integer window handles and maintains:
|
||||
@itemlist[#:style 'compact
|
||||
@item{@tt{_views}: handle → @tt{WebviewWindow*}}
|
||||
@item{@tt{_view_js_callbacks}: handle → host event callback}
|
||||
]
|
||||
}
|
||||
@item{Implements the backend of all operations (create/close, navigation, JS run/call, geometry, visibility, devtools, dialogs, title/state, OU token).}
|
||||
@item{Provides @emph{bounded} event-loop pumping so a non-Qt host can drive the GUI cooperatively.}
|
||||
@item{Periodically harvests page→host events by invoking JavaScript in each page.}
|
||||
]
|
||||
|
||||
@subsection{@tt{WebviewWindow} (Window) and @tt{WebViewQt} (View)}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@tt{WebviewWindow}: a @tt{QMainWindow} that embeds a @tt{WebViewQt}, handles lifecycle (show/hide/close), debounces geometry changes, opens devtools, processes certificate errors using an OU token, and periodically scrapes JS events from the page.}
|
||||
@item{@tt{WebViewQt}: a @tt{QWebEngineView} with a stable integer ID and a back-pointer to its parent window; participates in load signal wiring so the controller can inject helper JS and fire a @tt{"page-loaded"} event.}
|
||||
]
|
||||
|
||||
@subsection{Utility Layer}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@tt{EventContainer}: a lightweight key–value structure for assembling outbound events.}
|
||||
@item{@tt{mkEventJson}: serializes an @tt{EventContainer} to compact JSON text for the host callback.}
|
||||
]
|
||||
|
||||
@section{Threading, Event Loop, and Timing}
|
||||
|
||||
@subsection{Initialization and Timers}
|
||||
|
||||
On the first entry (or via explicit initialization), the controller constructs
|
||||
the @tt{QApplication} and sets up two key timers:
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@bold{JS event harvester} @tt{_process_events}: fires every few milliseconds to call into each page and fetch queued page→host events.}
|
||||
@item{@bold{Event-loop slice} @tt{_evt_loop_timer}: a single-shot timer used to exit a short run of the Qt event loop; this underpins the bounded pump.}
|
||||
]
|
||||
|
||||
@subsection{Bounded Event-Loop Pumping}
|
||||
|
||||
Because the host may not yield a classic Qt main loop, the controller exposes a
|
||||
“pump” that executes Qt’s event loop briefly, then exits via the single-shot
|
||||
timer. A host helper can call this repeatedly for a requested duration to:
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{Deliver posted @tt{CommandEvent}s to the GUI thread.}
|
||||
@item{Complete asynchronous WebEngine operations (e.g., JS callbacks, load signals).}
|
||||
@item{Run the JS harvester that drains page→host events.}
|
||||
]
|
||||
|
||||
This design preserves GUI responsiveness and enables synchronous host calls
|
||||
without blocking the GUI thread.
|
||||
|
||||
@section{Lifecycle and Window Semantics}
|
||||
|
||||
@subsection{Creation and Parenting}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{A “create” command constructs a @tt{WebviewWindow}, optionally modal if a valid parent handle is supplied.}
|
||||
@item{A @tt{WebViewQt} is created with a fresh handle and set as the central widget.}
|
||||
@item{The window is shown; the command returns only after the window reports @emph{created}, ensuring safe subsequent operations.}
|
||||
]
|
||||
|
||||
@subsection{Closing and the Blocked-Close Pattern}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{If the window is not yet permitted to close, a close request is ignored and a @tt{"can-close?"} event is emitted to the host.}
|
||||
@item{When permitted, the window closes, a @tt{"closed"} event is emitted, and the handle is removed from controller maps. Any devtools window is also disposed.}
|
||||
]
|
||||
|
||||
@subsection{Visibility and Activation}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@tt{show}, @tt{hide}, @tt{show-normal}, @tt{minimize}, @tt{maximize}, @tt{present}.}
|
||||
@item{@tt{present} ensures the window is shown, raised, and activated.}
|
||||
@item{Window state queries map Qt’s visibility/activation to a simplified enum, including @emph{active} variants.}
|
||||
]
|
||||
|
||||
@subsection{Geometry, Debounce, and Completion Criteria}
|
||||
|
||||
Move/resize semantics are designed to be deterministic:
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{Each @tt{move} or @tt{resize} triggers a 500ms single-shot timer that coalesces rapid changes (e.g., during drags).}
|
||||
@item{The command completes only after the window’s internal move/resize counter increments, guaranteeing a visible state change before success is reported.}
|
||||
@item{Corresponding @tt{"move"} and @tt{"resize"} events include stable geometry values @tt{x}, @tt{y}, @tt{w}, and @tt{h}.}
|
||||
]
|
||||
|
||||
@section{JavaScript Bridge}
|
||||
|
||||
@subsection{Injected Helpers and Event Queue}
|
||||
|
||||
On page load, the controller ensures the following helpers exist in the page
|
||||
context (application world), establishing a minimal event queue:
|
||||
|
||||
@verbatim|{
|
||||
window.rkt_event_queue = [];
|
||||
window.rkt_send_event = function(obj) {
|
||||
window.rkt_event_queue.push(obj);
|
||||
};
|
||||
window.rkt_get_events = function() {
|
||||
let q = window.rkt_event_queue;
|
||||
window.rkt_event_queue = [];
|
||||
return JSON.stringify(q);
|
||||
};
|
||||
}|
|
||||
|
||||
Notes:
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{The helpers are injected after page load so they are present for app code regardless of the navigated content.}
|
||||
@item{A previously prepared @tt{QWebEngineScript} injection path exists on the window, but the explicit insertion call is disabled; the active injection occurs via the controller on page load, which is sufficient in practice.}
|
||||
]
|
||||
|
||||
@subsection{Periodic Harvesting (Page → Host)}
|
||||
|
||||
A fast timer iterates active windows and evaluates @tt{rkt_get_events()} in each
|
||||
page. The returned JSON array is parsed; each object is wrapped as an
|
||||
@tt{EventContainer} of kind @tt{"js-evt"} (embedding the original object under
|
||||
a field) and emitted to the host callback as a compact JSON string.
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{This keeps the host callback signature simple (C string payload).}
|
||||
@item{Batching reduces cross-thread chatter; clearing the page-side queue prevents duplicates.}
|
||||
]
|
||||
|
||||
@subsection{Run vs Call (Host → Page)}
|
||||
|
||||
Two execution modes are differentiated to avoid conflating effects and results:
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@bold{Run}: fire-and-forget evaluation for side-effects (e.g., DOM mutations). No return value is captured.}
|
||||
@item{@bold{Call}: user code is wrapped in a @tt{try/catch} scaffold that returns a JSON object:
|
||||
@verbatim|{
|
||||
{
|
||||
"oke": <boolean>,
|
||||
"result": <any-or-false>,
|
||||
"exn": <string-or-false>
|
||||
}
|
||||
}|
|
||||
The native side returns this JSON as a newly allocated result object; @tt{js_result_ok} mirrors the @tt{"oke"} field.}
|
||||
]
|
||||
|
||||
@section{Dialogs and Security Controls}
|
||||
|
||||
@subsection{Native Dialogs}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@tt{choose-dir}: returns a compact JSON with either @tt{"choosen"} and a directory path, or @tt{"canceled"} with the base directory.}
|
||||
@item{@tt{file-open}, @tt{file-save}: similarly return @tt{"choosen"} with a file path and the filter used, or @tt{"canceled"} with the last-selected filter.}
|
||||
@item{The host must free returned result objects; see @secref{memory}.}
|
||||
]
|
||||
|
||||
@subsection{Certificate Handling with OU Token}
|
||||
|
||||
When a certificate error arises, the window inspects the chain; if it encounters
|
||||
a self-signed certificate whose Organizational Unit (OU) equals the configured
|
||||
token, it accepts the certificate.
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{This provides a controlled trust mechanism for private/dev deployments.}
|
||||
@item{Ordinary sites remain subject to standard verification.}
|
||||
]
|
||||
|
||||
@section{Event Model (Delivered to Host)}
|
||||
|
||||
All events are delivered as compact JSON strings to the host’s callback.
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@tt{"page-loaded"}: emitted after helper JS installation; includes an @tt{"oke"} boolean indicating load success.}
|
||||
@item{@tt{"show"} and @tt{"hide"}: visibility transitions (hide is suppressed on final close).}
|
||||
@item{@tt{"move"}: includes @tt{"x"}, @tt{"y"} after debounce.}
|
||||
@item{@tt{"resize"}: includes @tt{"w"}, @tt{"h"} after debounce.}
|
||||
@item{@tt{"can-close?"}: emitted when a close was requested but not yet permitted.}
|
||||
@item{@tt{"closed"}: emitted when the window is actually destroyed and unregistered.}
|
||||
@item{@tt{"js-evt"}: wrapper events for each page-originated object collected via the event queue.}
|
||||
]
|
||||
|
||||
@section{Command Set and Internal Behavior}
|
||||
|
||||
This section summarizes each command’s core behavior and completion rule.
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@bold{CREATE}: build @tt{WebviewWindow} (+ optional modal parent), create @tt{WebViewQt}, assign handle, @tt{show}, wait until @emph{created} flag is true, then return handle.}
|
||||
@item{@bold{CLOSE}: mark as allowed to close, call @tt{close()}, emit @tt{"closed"} on destruction, unregister handle.}
|
||||
@item{@bold{SET\_OU\_TOKEN}: stash OU token in the window for later certificate checks.}
|
||||
@item{@bold{SET\_URL}: @tt{QWebEngineView::setUrl}; return success/failure after dispatch.}
|
||||
@item{@bold{SET\_HTML}: @tt{QWebEngineView::setHtml}; return success/failure after dispatch.}
|
||||
@item{@bold{SET\_TITLE}: set the native window title; return success/failure.}
|
||||
@item{@bold{RUN\_JS}: invoke JavaScript without awaiting a value; return success/failure (evaluation dispatched).}
|
||||
@item{@bold{CALL\_JS}: wrap in @tt{try/catch}, return JSON object; success when callback delivers result, propagate @tt{oke} to @tt{js_result_ok}.}
|
||||
@item{@bold{DEV\_TOOLS}: open a separate window, attach the page’s devtools page, and keep it in sync; dispose with parent.}
|
||||
@item{@bold{SHOW/HIDE/PRESENT/MAXIMIZE/MINIMIZE/SHOW\_NORMAL}: call corresponding Qt methods and return @tt{oke} upon dispatch; @tt{present} additionally raises and activates.}
|
||||
@item{@bold{WINDOW\_STATUS}: map @tt{isHidden}/@tt{isMinimized}/@tt{isMaximized}/@tt{isVisible} (+ @tt{isActiveWindow}) to a simplified enum (including @emph{active} variants).}
|
||||
@item{@bold{MOVE}: call @tt{move(x,y)}, then wait until the move counter increments (debounced) before acknowledging success.}
|
||||
@item{@bold{RESIZE}: call @tt{resize(w,h)}, then wait until the resize counter increments (debounced) before acknowledging success.}
|
||||
@item{@bold{CHOOSE\_DIR / FILE\_OPEN / FILE\_SAVE}: open native dialogs, convert result to compact JSON, return as newly allocated result object.}
|
||||
]
|
||||
|
||||
@section[#:tag "memory"]{Memory, Ownership, and Boundaries}
|
||||
@;@seclabel{memory}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@italic{Events} and @italic{JS-call results} are returned as small C structs containing freshly allocated UTF‐8 strings; the host must free them using the provided destroy helpers.}
|
||||
@item{Qt types remain internal; boundary values are converted to/from UTF‐8 @tt{char*}.}
|
||||
@item{Commands are transient and owned by the GUI thread; the host never owns a @tt{Command}.}
|
||||
]
|
||||
|
||||
@section{Determinism, Failure Modes, and Guarantees}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{All UI-affecting operations execute on the GUI thread via posted commands (with normal priority), ensuring thread safety and sequential handling as the event queue is processed by Qt in order of posting.}
|
||||
@item{Synchronous host calls return only after a definitive outcome:
|
||||
@itemlist[#:style 'compact
|
||||
@item{Navigation/HTML: boolean success/failure.}
|
||||
@item{JS run: success/failure of dispatch.}
|
||||
@item{JS call: structured JSON with @tt{oke}/@tt{result}/@tt{exn}.}
|
||||
@item{Geometry: acknowledgment only after debounced counters change.}
|
||||
@item{Dialogs: compact JSON describing @tt{"choosen"} or @tt{"canceled"}.}
|
||||
]
|
||||
}
|
||||
@item{State queries (e.g., window state) are also executed as commands, reflecting the GUI thread’s authoritative view.}
|
||||
]
|
||||
|
||||
@section{Edge Cases and Notes}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{Helper JS can be injected via two mechanisms; the active path injects after page load from the controller. A prebuilt @tt{QWebEngineScript} insertion in the window is present but disabled.}
|
||||
@item{Debounce windows (500ms timers) intentionally delay geometry events and command completion under heavy interaction, trading a tiny latency for stability and reduced churn.}
|
||||
@item{Host-driven pumps should be called frequently enough to maintain UI responsiveness and timely delivery of JS results and events.}
|
||||
]
|
||||
|
||||
@section{Worked Walkthrough (Typical Session)}
|
||||
|
||||
A typical host session proceeds like this:
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{Initialize controller (implicit on first call).}
|
||||
@item{Create window A; move/resize; set URL or HTML.}
|
||||
@item{Pump events in a loop; open devtools if needed.}
|
||||
@item{Call JS to compute a value (returns structured JSON) and run JS for side-effects (fire-and-forget).}
|
||||
@item{From page scripts, call @tt{rkt_send_event({...})}; the host periodically receives @tt{"js-evt"} payloads during pumps.}
|
||||
@item{Optionally create window B as a modal child of A; navigate separately; continue pumping.}
|
||||
@item{Close B; later close A (possibly after a @tt{"can-close?"} handshake).}
|
||||
]
|
||||
|
||||
@section{Extensibility Guidelines}
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{For new features, add a command code, implement a @tt{processCommand} case, and provide a controller wrapper that constructs the command and awaits completion.}
|
||||
@item{Prefer compact JSON for multi-field results to keep the host boundary stable and language-agnostic.}
|
||||
@item{If adding richer page bridges, inject helpers on load and keep harvesting batched and periodic to minimize overhead.}
|
||||
@item{Maintain the convention that all externally observable events include an explicit @tt{"event"} tag plus relevant fields.}
|
||||
]
|
||||
|
||||
@section{Summary}
|
||||
|
||||
Internally, this bridge is a command-queued, GUI-thread–safe façade over Qt
|
||||
WebEngine. The host sees synchronous operations; under the hood, each action is
|
||||
posted to the GUI thread, the event loop is pumped in short slices, and
|
||||
completion is acknowledged only after the intended UI effect is verifiably in
|
||||
place. A small JSON bridge carries page→host messages and supports synchronous
|
||||
host→page calls with structured results, keeping the boundary stable and
|
||||
language-neutral.
|
||||
Reference in New Issue
Block a user