This commit is contained in:
2026-03-09 09:28:46 +01:00
parent 0ec5a42c71
commit a078022401
7 changed files with 673 additions and 19 deletions

4
.gitignore vendored
View File

@@ -20,3 +20,7 @@ webview/
rktwebview/build/ rktwebview/build/
/rktwebview/.qtcreator/CMakeLists.txt.user /rktwebview/.qtcreator/CMakeLists.txt.user
/private/*.bak /private/*.bak
/scrbl/*.css
/scrbl/*.js
*.bak
/scrbl/*.html

View File

@@ -32,7 +32,8 @@ typedef enum {
resize_failed = 13, resize_failed = 13,
choose_dir_failed = 14, choose_dir_failed = 14,
open_file_failed = 15, open_file_failed = 15,
save_file_failed = 16 save_file_failed = 16,
failed = 17
} result_t; } result_t;
typedef struct { typedef struct {

View File

@@ -424,7 +424,7 @@ result_t Rktwebview_qt::rktSetHtml(rktwebview_t wv, const char *html)
bool r = c.result.toBool(); bool r = c.result.toBool();
return r ? result_t::oke : result_t::set_navigate_failed; return r ? result_t::oke : result_t::set_html_failed;
} }
rkt_js_result_t *Rktwebview_qt::rktCallJs(rktwebview_t wv, const char *js) rkt_js_result_t *Rktwebview_qt::rktCallJs(rktwebview_t wv, const char *js)
@@ -481,32 +481,32 @@ result_t Rktwebview_qt::rktResize(rktwebview_t wv, int w, int h)
result_t Rktwebview_qt::rktHideWindow(rktwebview_t w) result_t Rktwebview_qt::rktHideWindow(rktwebview_t w)
{ {
return doWindow(w, COMMAND_HIDE_WIN); return doWindow(w, COMMAND_HIDE_WIN, result_t::failed);
} }
result_t Rktwebview_qt::rktShowWindow(rktwebview_t w) result_t Rktwebview_qt::rktShowWindow(rktwebview_t w)
{ {
return doWindow(w, COMMAND_SHOW_WIN); return doWindow(w, COMMAND_SHOW_WIN, result_t::failed);
} }
result_t Rktwebview_qt::rktPresentWindow(rktwebview_t w) result_t Rktwebview_qt::rktPresentWindow(rktwebview_t w)
{ {
return doWindow(w, COMMAND_PRESENT_WIN); return doWindow(w, COMMAND_PRESENT_WIN, result_t::failed);
} }
result_t Rktwebview_qt::rktMaximizeWindow(rktwebview_t w) result_t Rktwebview_qt::rktMaximizeWindow(rktwebview_t w)
{ {
return doWindow(w, COMMAND_MAX_WIN); return doWindow(w, COMMAND_MAX_WIN, result_t::failed);
} }
result_t Rktwebview_qt::rktMinimizeWindow(rktwebview_t w) result_t Rktwebview_qt::rktMinimizeWindow(rktwebview_t w)
{ {
return doWindow(w, COMMAND_MIN_WIN); return doWindow(w, COMMAND_MIN_WIN, result_t::failed);
} }
result_t Rktwebview_qt::rktShowNormalWindow(rktwebview_t w) result_t Rktwebview_qt::rktShowNormalWindow(rktwebview_t w)
{ {
return doWindow(w, COMMAND_SHOW_NORMAL_WIN); return doWindow(w, COMMAND_SHOW_NORMAL_WIN, result_t::failed);
} }
window_state_t Rktwebview_qt::rktWindowState(rktwebview_t w) window_state_t Rktwebview_qt::rktWindowState(rktwebview_t w)
@@ -609,17 +609,17 @@ result_t Rktwebview_qt::rktWindowSetTitle(rktwebview_t wv, const char *title)
postCommand(&c); postCommand(&c);
while(!c.done) { doEvents(); } while(!c.done) { doEvents(); }
bool r = c.result.toBool(); bool r = c.result.toBool();
return r ? result_t::oke : result_t::resize_failed; return r ? result_t::oke : result_t::failed;
} }
result_t Rktwebview_qt::doWindow(rktwebview_t w, int cmd) result_t Rktwebview_qt::doWindow(rktwebview_t w, int cmd, result_t on_error)
{ {
Command c(cmd); Command c(cmd);
c.args.push_back(w); c.args.push_back(w);
postCommand(&c); postCommand(&c);
while(!c.done) { doEvents(); } while(!c.done) { doEvents(); }
bool r = c.result.toBool(); bool r = c.result.toBool();
return r ? result_t::oke : result_t::resize_failed; return r ? result_t::oke : on_error;
} }
bool Rktwebview_qt::rktValid(rktwebview_t wv) bool Rktwebview_qt::rktValid(rktwebview_t wv)

View File

@@ -41,7 +41,7 @@ private:
private: private:
void runJs(rktwebview_t wv, const char *js); void runJs(rktwebview_t wv, const char *js);
result_t doWindow(rktwebview_t w, int cmd); result_t doWindow(rktwebview_t w, int cmd, result_t on_error);
public slots: public slots:
void processJsEventQueues(); void processJsEventQueues();

View File

@@ -150,13 +150,6 @@ void WebviewWindow::addView(WebViewQt *v, Rktwebview_qt *c)
" let json_q = JSON.stringify(q);\n" " let json_q = JSON.stringify(q);\n"
" return json_q;\n" " return json_q;\n"
"};\n" "};\n"
"console.log('We have set:');\n"
"console.log('window.rkt_event_queue:');\n"
"console.log(window.rkt_event_queue);\n"
"console.log('window.rkt_send_event:');\n"
"console.log(window.rkt_send_event);\n"
"console.log('window.rkt_get_events:');\n"
"console.log(window.rkt_get_events);\n"
); );
evt_script.setWorldId(QWebEngineScript::ApplicationWorld); evt_script.setWorldId(QWebEngineScript::ApplicationWorld);
//col.insert(evt_script); //col.insert(evt_script);

348
scrbl/rktwebview-api.scrbl Normal file
View File

@@ -0,0 +1,348 @@
#lang scribble/manual
@title{Public API Specification for the QtBacked WebView Bridge}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@italic{For use with Racket FFI}
This document defines the public C-level API of the Qt-based WebView bridge.
It targets Racket developers integrating the C library via the Racket FFI.
It specifies the interface, result values, memory ownership rules, event formats,
expected behavior, and recommended calling patterns. Internal architecture is documented separately.
@section{Calling Convention and Structure}
All functions are C functions with normal C linkage.
Window handles are simple integers that uniquely identify native windows.
@itemlist[#:style 'compact
@item{Header: @tt{rktwebview.h}}
@item{ABI: plain C, no name mangling}
@item{Handles remain valid from creation until the window is fully closed}
]
@section{Opaque and Helper Types}
@subsection{Window Handle}
@itemlist[#:style 'compact
@item{@tt{typedef int rktwebview_t}: Valid for any window created by @tt{rkt_webview_create} until closed.}
]
@subsection{Event Callback Types}
@itemlist[#:style 'compact
@item{@tt{typedef struct \{ rktwebview_t wv; char* event; \} rkt_event_t}: JSON event payload for the host (UTF8).}
@item{@tt{typedef void (*event_cb_t)(rkt_event_t*)}: Host-supplied callback for event delivery.}
]
@subsection{JS Result Structure}
@itemlist[#:style 'compact
@item{@tt{typedef struct \{ result_t result; char* value; \} rkt_js_result_t}: @tt{result} is always @tt{oke} or @tt{eval_js_failed}; @tt{value} holds a UTF8 JSON string describing the JS outcome.}
]
@section{Enumerations}
@subsection{result_t}
@itemlist[#:style 'compact
@item{@tt{oke = 0}}
@item{@tt{set_html_failed = 1}}
@item{@tt{set_navigate_failed = 2}}
@item{@tt{eval_js_failed = 3}}
@item{@tt{move_failed = 12}}
@item{@tt{resize_failed = 13}}
@item{@tt{choose_dir_failed = 14}}
@item{@tt{open_file_failed = 15}}
@item{@tt{save_file_failed = 16}}
@item{@tt{failed = 17}}
]
@subsection{window_state_t}
@itemlist[#:style 'compact
@item{@tt{invalid = -1}}
@item{@tt{normal = 0}}
@item{@tt{minimized = 1}}
@item{@tt{maximized = 2}}
@item{@tt{hidden = 3}}
@item{@tt{normal_active = 16}}
@item{@tt{maximized_active = 18}}
]
@section{Threading Model}
@itemlist[#:style 'compact
@item{All GUI work occurs on the Qt GUI thread.}
@item{Public calls block until the posted internal command has completed.}
@item{The host is expected to drive Qt by calling @tt{rkt_webview_process_events}.}
]
@section{Memory Ownership}
@itemlist[#:style 'compact
@item{@tt{rkt_event_t*} must be freed using @tt{rkt_webview_destroy_event}.}
@item{@tt{rkt_js_result_t*} must be freed using @tt{rkt_webview_destroy_js_result}.}
@item{All returned strings are UTF8 and owned by the library until destroyed with the corresponding function.}
]
@section{Function Reference (C signatures)}
The following are literal C signatures with behavioral descriptions.
@subsection{Initialization and Event Pumping}
@codeblock{
void rkt_webview_init(void);
}
Initializes Qt and internal state. Safe to call multiple times (subsequent calls are no-ops).
@codeblock{
void rkt_webview_process_events(int for_ms);
}
Pumps the Qt event loop for approximately @tt{for_ms} milliseconds.
Idea for implementation in racket:
Use a @emph{cooperative thread} with @emph{time slicing}. A practical starting point:
@itemlist[#:style 'compact
@item{~3 ms Qt pumping per iteration: @tt{(rkt_webview_process_events 3)}}
@item{~10 ms Racket time (scheduler): @tt{(sleep 0.01)}}
]
Tune these values for your application.
Example:
@verbatim|{
(define running? (box #t))
(define (pump-loop)
(let loop ()
(when (unbox running?)
(rkt_webview_process_events 3) ; ~3 ms Qt
(sleep 0.01) ; ~10 ms for Racket scheduler
(loop))))
(define pump-thread (thread pump-loop))
;; ... later on shutdown:
(set-box! running? #f)
(thread-wait pump-thread)
}|
@subsection{Window Lifecycle}
@codeblock{
rktwebview_t rkt_webview_create(rktwebview_t parent, event_cb_t cb);
}
Creates a window, optionally modal if @tt{parent} exists. Registers @tt{cb}.
The window is shown before returning.
@codeblock{
void rkt_webview_close(rktwebview_t wv);
}
Requests closure of @tt{wv}.
Behavior:
@itemlist[#:style 'compact
@item{If the user tries to close via the window manager (close button, AltF4, etc.), the library intercepts it and emits @tt{"can-close?"}. The window remains open.}
@item{To actually close after receiving @tt{"can-close?"}, the host must call @tt{rkt_webview_close}.}
@item{When the final close occurs, @tt{"closed"} is emitted and the handle becomes invalid.}
]
@codeblock{
bool rkt_webview_valid(rktwebview_t wv);
}
Returns whether @tt{wv} refers to an active window.
@subsection{Navigation and Content}
@codeblock{
result_t rkt_webview_set_url(rktwebview_t wv, const char* url);
}
Navigate to @tt{url}. Returns @tt{oke} or @tt{set_navigate_failed}.
@codeblock{
result_t rkt_webview_set_html(rktwebview_t wv, const char* html);
}
Set page content from @tt{html}. Returns @tt{oke} or @tt{set_html_failed}.
@codeblock{
result_t rkt_webview_set_title(rktwebview_t wv, const char* title);
}
Set the native window title. Returns @tt{oke} or @tt{failed}.
@subsection{JavaScript Execution}
@codeblock{
result_t rkt_webview_run_js(rktwebview_t wv, const char* js);
}
Fire-and-forget JavaScript execution. Returns @tt{oke} or @tt{eval_js_failed}.
@codeblock{
rkt_js_result_t* rkt_webview_call_js(rktwebview_t wv, const char* js);
}
Synchronous JS call. Guarantees:
@itemlist[#:style 'compact
@item{@tt{result} is @tt{oke} or @tt{eval_js_failed}.}
@item{@tt{value} is JSON: @tt{{"oke": <bool>, "result": <value-or-false>, "exn": <string-or-false>}}.}
]
Must be freed with @tt{rkt_webview_destroy_js_result()}.
@codeblock{
result_t rkt_webview_destroy_js_result(rkt_js_result_t* r);
}
Frees JS result memory. Returns @tt{oke}.
@subsection{Developer Tools}
@codeblock{
result_t rkt_webview_open_devtools(rktwebview_t wv);
}
Open devtools. Returns @tt{oke} or @tt{no_devtools_on_platform} (platform-dependent).
@subsection{Window Geometry and Visibility}
@codeblock{
result_t rkt_webview_move(rktwebview_t wv, int x, int y);
}
Waits until move is acknowledged. Returns @tt{oke} or @tt{move_failed}.
@codeblock{
result_t rkt_webview_resize(rktwebview_t wv, int w, int h);
}
Waits until resize is acknowledged. Returns @tt{oke} or @tt{resize_failed}.
@codeblock{
result_t rkt_webview_hide(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_show(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_show_normal(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_present(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_maximize(rktwebview_t wv);
}
@codeblock{
result_t rkt_webview_minimize(rktwebview_t wv);
}
All return @tt{oke} or @tt{failed} (when the operation cannot be applied).
@codeblock{
window_state_t rkt_webview_window_state(rktwebview_t wv);
}
Returns a simplified window state.
@subsection{Event & Dialog Systems}
@codeblock{
result_t rkt_webview_destroy_event(rkt_event_t* e);
}
Frees event memory. Returns @tt{oke}.
@subsection{Native Dialogs}
@codeblock{
rkt_js_result_t* rkt_webview_choose_dir(rktwebview_t wv, const char* title, const char* base_dir);
}
Returns JSON:
@itemlist[#:style 'compact
@item{@tt{"state":"choosen","dir":"..."}}
@item{@tt{"state":"canceled","dir":"..."}}
]
Must be freed with @tt{rkt_webview_destroy_js_result}.
@codeblock{
rkt_js_result_t* rkt_webview_file_open(rktwebview_t wv, const char* title, const char* base_dir, const char* permitted_exts);
}
@codeblock{
rkt_js_result_t* rkt_webview_file_save(rktwebview_t wv, const char* title, const char* base_dir, const char* permitted_exts);
}
Return JSON:
@itemlist[#:style 'compact
@item{@tt{"state":"choosen","file":"...","used-filter":"..."}}
@item{@tt{"state":"canceled","file":"","used-filter":"..."}}
]
Must be freed with @tt{rkt_webview_destroy_js_result}.
@subsection{Security}
@codeblock{
void rkt_webview_set_ou_token(rktwebview_t wv, const char* token);
}
This function configures a per-window "OU acceptance token" that affects how
certificate errors are handled inside the internal @tt{QWebEnginePage}.
When a TLS certificate error occurs, the internal page examines the certificate
chain. If all the following conditions are met:
@itemlist[#:style 'compact
@item{the certificate is self-signed,}
@item{the certificate contains an Organizational Unit (OU) field,}
@item{the OU value exactly matches the token previously set with @tt{rkt_webview_set_ou_token},}
]
then the certificate is programmatically accepted and the load is allowed to
continue.
This mechanism is intended for controlled environments (e.g., local development
servers or private deployments) where a known self-signed certificate is used
and its OU value is part of the trust boundary.
Note: starting with Qt 6.10, @tt{QWebEnginePage} introduces an API for providing
a specific certificate to be accepted directly by the page. However, this API is
very new ("bleeding edge") and not yet used or relied upon in this WebView
bridge.
@section{Event JSON Format}
@itemlist[#:style 'compact
@item{@tt{"event":"page-loaded","oke": <bool>}}
@item{@tt{"event":"show"}}
@item{@tt{"event":"hide"}}
@item{@tt{"event":"move","x": <int>,"y": <int>}}
@item{@tt{"event":"resize","w": <int>,"h": <int>}}
@item{@tt{"event":"can-close?"}}
@item{@tt{"event":"closed"}}
@item{@tt{"event":"js-evt","js-evt": <object>}}
]
@section{Typical Usage Pattern}
@itemlist[#:style 'compact
@item{Initialize once.}
@item{Create windows.}
@item{Run a cooperative time-sliced pump loop (e.g., ~3 ms Qt, ~10 ms Racket).}
@item{Invoke JS run/call functions.}
@item{Handle and free events.}
@item{Close windows explicitly.}
]
@section{Summary}
This API provides a stable, simple interface for embedding a Qt WebEngine
webview inside a Racket program via FFI. The API is synchronous, uses compact
JSON for results, and defines clear memory ownership rules.
``

View File

@@ -0,0 +1,308 @@
#lang scribble/manual
@title{Internal Architecture and Behavior of the QtBacked 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 WebEnginebacked webview bridge. It focuses on how
a synchronous-looking host API is implemented atop Qts 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 callers 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 keyvalue 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 Qts 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 Qts 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 windows 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 hosts 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 commands 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 pages 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 UTF8 strings; the host must free them using the provided destroy helpers.}
@item{Qt types remain internal; boundary values are converted to/from UTF8 @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 threads 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-threadsafe 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.