documentation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
# DrRacket autosave files
|
# DrRacket autosave files
|
||||||
*.rkt~
|
*.rkt~
|
||||||
|
*.scrbl~
|
||||||
*.rkt.bak
|
*.rkt.bak
|
||||||
\#*.rkt#
|
\#*.rkt#
|
||||||
\#*.rkt#*#
|
\#*.rkt#*#
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
(letrec ((f (λ ()
|
(letrec ((f (λ ()
|
||||||
(when go-on-counter
|
(when go-on-counter
|
||||||
(send this inc-counter)
|
(send this inc-counter)
|
||||||
(sleep 0.1)
|
(sleep 0.01)
|
||||||
(f)))))
|
(f)))))
|
||||||
(set! go-on-counter #t)
|
(set! go-on-counter #t)
|
||||||
(f)))))
|
(f)))))
|
||||||
@@ -197,8 +197,8 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
(define/public (prefs)
|
(define/public (prefs)
|
||||||
(stop-counter)
|
;(stop-counter)
|
||||||
(set! go-on-counter #f)
|
;(set! go-on-counter #f)
|
||||||
(new example-1-dialog%
|
(new example-1-dialog%
|
||||||
[parent this]
|
[parent this]
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
@@ -583,7 +583,7 @@
|
|||||||
(define (rkt-webview-valid? wv)
|
(define (rkt-webview-valid? wv)
|
||||||
(if (eq? (rkt-wv-valid wv) #f)
|
(if (eq? (rkt-wv-valid wv) #f)
|
||||||
#f
|
#f
|
||||||
(if (= (rkt_webview_valid wv) 0)
|
(if (= (rkt_webview_valid (rkt-wv-win wv)) 0)
|
||||||
#f
|
#f
|
||||||
#t)))
|
#t)))
|
||||||
|
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (i == 6) {
|
if (i == 6) {
|
||||||
rkt_data_t *r = rkt_webview_call_js(wv1, "document.body.innerHTML = '<h1>Hi!</h1>'; return document.body.innerHTML;");
|
//rkt_data_t *r = rkt_webview_call_js(wv1, "document.body.innerHTML = '<h1>Hi!</h1>'; return document.body.innerHTML;");
|
||||||
printf("rkt_js_result: %d: %s\n", r->data.js_result.result, r->data.js_result.value);
|
//printf("rkt_js_result: %d: %s\n", r->data.js_result.result, r->data.js_result.value);
|
||||||
rkt_webview_free_data(r);
|
//rkt_webview_free_data(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == 7) {
|
if (i == 7) {
|
||||||
@@ -75,7 +75,7 @@ int main(int argc, char *argv[])
|
|||||||
rkt_webview_run_js(wv1, buf);
|
rkt_webview_run_js(wv1, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == 24) {
|
if (i == 15) {
|
||||||
rkt_webview_close(wv2);
|
rkt_webview_close(wv2);
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|||||||
@@ -352,23 +352,10 @@ void Rktwebview_qt::processCommand(Command *cmd)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rktwebview_qt::processJsEventQueues()
|
|
||||||
{
|
|
||||||
QList<rktwebview_t> wvs = _views.keys();
|
|
||||||
int i, N;
|
|
||||||
for(i = 0, N = wvs.length(); i < N; i++) {
|
|
||||||
int wv = wvs[i];
|
|
||||||
if (_views.contains(wv)) {
|
|
||||||
WebviewWindow *win = _views[wv];
|
|
||||||
win->processJsEvents();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Rktwebview_qt::removeView(int id)
|
void Rktwebview_qt::removeView(int id)
|
||||||
{
|
{
|
||||||
if (_views.contains(id)) {
|
if (_views.contains(id)) {
|
||||||
WebviewWindow *win = _views[id];
|
//WebviewWindow *win = _views[id];
|
||||||
_views.remove(id);
|
_views.remove(id);
|
||||||
_view_js_callbacks.remove(id);
|
_view_js_callbacks.remove(id);
|
||||||
}
|
}
|
||||||
@@ -394,9 +381,14 @@ rkt_wv_context_t Rktwebview_qt::newContext(const char *boilerplate_js, const cha
|
|||||||
QString name = QString::asprintf("profile-%d", _context_counter);
|
QString name = QString::asprintf("profile-%d", _context_counter);
|
||||||
|
|
||||||
QString code = "if (window.rkt_event_queue === undefined) { window.rkt_event_queue = []; }\n"
|
QString code = "if (window.rkt_event_queue === undefined) { window.rkt_event_queue = []; }\n"
|
||||||
|
"window.rkt_evt_frame_el = null;\n"
|
||||||
|
"window.rkt_evt_frame_win = null;\n"
|
||||||
"window.rkt_send_event = function(obj) {\n"
|
"window.rkt_send_event = function(obj) {\n"
|
||||||
" console.log('Sending event: ' + obj);\n"
|
" //console.log('Sending event: ' + obj);\n"
|
||||||
" window.rkt_event_queue.push(obj);\n"
|
" window.rkt_event_queue.push(obj);\n"
|
||||||
|
" if (window.rkt_evt_frame_el) {\n"
|
||||||
|
" window.rkt_evt_frame_win.print();\n"
|
||||||
|
" }\n"
|
||||||
"};\n"
|
"};\n"
|
||||||
"window.rkt_get_events = function() {\n"
|
"window.rkt_get_events = function() {\n"
|
||||||
" let q = window.rkt_event_queue;\n"
|
" let q = window.rkt_event_queue;\n"
|
||||||
@@ -404,6 +396,22 @@ rkt_wv_context_t Rktwebview_qt::newContext(const char *boilerplate_js, const cha
|
|||||||
" let json_q = JSON.stringify(q);\n"
|
" let json_q = JSON.stringify(q);\n"
|
||||||
" return json_q;\n"
|
" return json_q;\n"
|
||||||
"};\n"
|
"};\n"
|
||||||
|
"// add hidden hover element to body if necessary\n"
|
||||||
|
"setInterval(function () {\n"
|
||||||
|
" if (window.rkt_evt_frame_el === null || window.rkt_evt_frame_el === undefined) {\n"
|
||||||
|
" window.rkt_evt_frame_el = document.createElement('iframe');\n"
|
||||||
|
" window.rkt_evt_frame_el.style.display = 'none';\n"
|
||||||
|
" window.rkt_evt_frame_el.setAttribute('id', 'rkt-evt-frame');\n"
|
||||||
|
" window.rkt_evt_frame_el.setAttribute('name', 'rkt-evt-frame');\n"
|
||||||
|
" document.body.append(window.rkt_evt_frame_el);\n"
|
||||||
|
" window.rkt_evt_frame_win = window.rkt_evt_frame_el.contentWindow;\n"
|
||||||
|
" } else {"
|
||||||
|
" if (window.rkt_event_queue.length > 0) {\n"
|
||||||
|
" window.rkt_evt_frame_win.print();\n"
|
||||||
|
" }\n"
|
||||||
|
" }\n"
|
||||||
|
"},\n"
|
||||||
|
"10);\n"
|
||||||
"";
|
"";
|
||||||
|
|
||||||
QList<QWebEngineScript> scripts;
|
QList<QWebEngineScript> scripts;
|
||||||
@@ -767,24 +775,14 @@ void Rktwebview_qt::onPageLoad(rktwebview_t w)
|
|||||||
|
|
||||||
void Rktwebview_qt::pageLoaded(rktwebview_t w, bool ok)
|
void Rktwebview_qt::pageLoaded(rktwebview_t w, bool ok)
|
||||||
{
|
{
|
||||||
/*runJs(w,
|
|
||||||
"if (window.rkt_event_queue === undefined) { window.rkt_event_queue = []; }\n"
|
|
||||||
"window.rkt_send_event = function(obj) {\n"
|
|
||||||
" console.log('Sending event: ' + obj);\n"
|
|
||||||
" window.rkt_event_queue.push(obj);\n"
|
|
||||||
"};\n"
|
|
||||||
"window.rkt_get_events = function() {\n"
|
|
||||||
" let q = window.rkt_event_queue;\n"
|
|
||||||
" window.rkt_event_queue = [];\n"
|
|
||||||
" let json_q = JSON.stringify(q);\n"
|
|
||||||
" return json_q;\n"
|
|
||||||
"};\n"
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// Inject code of the profile to this page
|
// Inject code of the profile to this page
|
||||||
WebviewWindow *win = _views[w];
|
WebviewWindow *win = _views[w];
|
||||||
|
|
||||||
|
if (win->navigationEventSent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QWebEngineProfile *p = win->profile();
|
QWebEngineProfile *p = win->profile();
|
||||||
QWebEngineScriptCollection *col = p->scripts();
|
QWebEngineScriptCollection *col = p->scripts();
|
||||||
QList<QWebEngineScript> l = col->toList();
|
QList<QWebEngineScript> l = col->toList();
|
||||||
@@ -906,9 +904,6 @@ Rktwebview_qt::Rktwebview_qt(Rktwebview_qt **handler) :
|
|||||||
_evt_loop_depth = 0;
|
_evt_loop_depth = 0;
|
||||||
_app = new QApplication(_argc, _argv);
|
_app = new QApplication(_argc, _argv);
|
||||||
|
|
||||||
connect(&_process_events, &QTimer::timeout, this, &Rktwebview_qt::processJsEventQueues);
|
|
||||||
_process_events.start(5);
|
|
||||||
|
|
||||||
// See Qt 6.10 remark at doEvents.
|
// See Qt 6.10 remark at doEvents.
|
||||||
//connect(&_evt_loop_timer, &QTimer::timeout, this, &Rktwebview_qt::stopEventloop);
|
//connect(&_evt_loop_timer, &QTimer::timeout, this, &Rktwebview_qt::stopEventloop);
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ 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 on_error);
|
result_t doWindow(rktwebview_t w, int cmd, result_t on_error);
|
||||||
|
|
||||||
public slots:
|
|
||||||
void processJsEventQueues();
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void stopEventloop();
|
void stopEventloop();
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ WebviewWindow::WebviewWindow(QWebEngineProfile *profile, WebviewWindow *parent)
|
|||||||
_resized = 0;
|
_resized = 0;
|
||||||
|
|
||||||
_profile = profile;
|
_profile = profile;
|
||||||
|
_navigation_event_sent = false;
|
||||||
|
|
||||||
if (parent != nullptr) {
|
if (parent != nullptr) {
|
||||||
setWindowModality(Qt::WindowModality::WindowModal);
|
setWindowModality(Qt::WindowModality::WindowModal);
|
||||||
@@ -67,11 +68,14 @@ void WebviewWindow::navigationRequested(QWebEngineNavigationRequest &req)
|
|||||||
QWebEngineNavigationRequest::NavigationType t = req.navigationType();
|
QWebEngineNavigationRequest::NavigationType t = req.navigationType();
|
||||||
if (t == QWebEngineNavigationRequest::NavigationType::TypedNavigation ||
|
if (t == QWebEngineNavigationRequest::NavigationType::TypedNavigation ||
|
||||||
t == QWebEngineNavigationRequest::RedirectNavigation) {
|
t == QWebEngineNavigationRequest::RedirectNavigation) {
|
||||||
|
_navigation_event_sent = false;
|
||||||
req.accept();
|
req.accept();
|
||||||
} else {
|
} else {
|
||||||
EventContainer e("navigation-request");
|
EventContainer e("navigation-request");
|
||||||
|
|
||||||
e["url"] = req.url().toString();
|
QString u = req.url().toString();
|
||||||
|
|
||||||
|
e["url"] = u;
|
||||||
|
|
||||||
QString type;
|
QString type;
|
||||||
switch (req.navigationType()) {
|
switch (req.navigationType()) {
|
||||||
@@ -94,6 +98,8 @@ void WebviewWindow::navigationRequested(QWebEngineNavigationRequest &req)
|
|||||||
e["type"] = type;
|
e["type"] = type;
|
||||||
_container->triggerEvent(_view->id(), e);
|
_container->triggerEvent(_view->id(), e);
|
||||||
|
|
||||||
|
_navigation_event_sent = true;
|
||||||
|
|
||||||
req.reject();
|
req.reject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +214,11 @@ void WebviewWindow::setOUToken(const QString &token)
|
|||||||
_ou_token = token;
|
_ou_token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WebviewWindow::navigationEventSent()
|
||||||
|
{
|
||||||
|
return _navigation_event_sent;
|
||||||
|
}
|
||||||
|
|
||||||
void WebviewWindow::addView(WebViewQt *v, Rktwebview_qt *c)
|
void WebviewWindow::addView(WebViewQt *v, Rktwebview_qt *c)
|
||||||
{
|
{
|
||||||
_container = c;
|
_container = c;
|
||||||
@@ -216,36 +227,6 @@ void WebviewWindow::addView(WebViewQt *v, Rktwebview_qt *c)
|
|||||||
|
|
||||||
QWebEnginePage *page = _view->page();
|
QWebEnginePage *page = _view->page();
|
||||||
|
|
||||||
/*
|
|
||||||
if (_profile == nullptr) {
|
|
||||||
page = _view->page();
|
|
||||||
} else {
|
|
||||||
page = new QWebEnginePage(_profile, this);
|
|
||||||
_view->setPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject event handling code for the javascript side
|
|
||||||
QWebEngineScriptCollection &col = page->scripts();
|
|
||||||
QWebEngineScript evt_script;
|
|
||||||
evt_script.setInjectionPoint(QWebEngineScript::DocumentReady);
|
|
||||||
evt_script.setName("rkt_webview_event_handling");
|
|
||||||
evt_script.setSourceCode(
|
|
||||||
"window.rkt_event_queue = [];\n"
|
|
||||||
"window.rkt_send_event = function(obj) {\n"
|
|
||||||
" console.log('Sending event: ' + obj);\n"
|
|
||||||
" window.rkt_event_queue.push(obj);\n"
|
|
||||||
"};\n"
|
|
||||||
"window.rkt_get_events = function() {\n"
|
|
||||||
" let q = window.rkt_event_queue;\n"
|
|
||||||
" window.rkt_event_queue = [];\n"
|
|
||||||
" let json_q = JSON.stringify(q);\n"
|
|
||||||
" return json_q;\n"
|
|
||||||
"};\n"
|
|
||||||
);
|
|
||||||
evt_script.setWorldId(QWebEngineScript::ApplicationWorld);
|
|
||||||
//col.insert(evt_script);
|
|
||||||
*/
|
|
||||||
|
|
||||||
connect(page, &QWebEnginePage::loadFinished, this, [this](bool ok) {
|
connect(page, &QWebEnginePage::loadFinished, this, [this](bool ok) {
|
||||||
_container->pageLoaded(_view->id(), ok);
|
_container->pageLoaded(_view->id(), ok);
|
||||||
});
|
});
|
||||||
@@ -255,6 +236,12 @@ void WebviewWindow::addView(WebViewQt *v, Rktwebview_qt *c)
|
|||||||
connect(page, &QWebEnginePage::navigationRequested, this, &WebviewWindow::navigationRequested);
|
connect(page, &QWebEnginePage::navigationRequested, this, &WebviewWindow::navigationRequested);
|
||||||
|
|
||||||
connect(page, &QWebEnginePage::certificateError, this, &WebviewWindow::handleCertificate);
|
connect(page, &QWebEnginePage::certificateError, this, &WebviewWindow::handleCertificate);
|
||||||
|
|
||||||
|
connect(page, &QWebEnginePage::windowCloseRequested, this, &WebviewWindow::closeView);
|
||||||
|
|
||||||
|
connect(page, &QWebEnginePage::linkHovered, this, &WebviewWindow::linkHovered);
|
||||||
|
|
||||||
|
connect(page, &QWebEnginePage::printRequestedByFrame, this, &WebviewWindow::evtRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
WebViewQt *WebviewWindow::view()
|
WebViewQt *WebviewWindow::view()
|
||||||
@@ -306,6 +293,21 @@ void WebviewWindow::openDevTools()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WebviewWindow::linkHovered(const QUrl &u)
|
||||||
|
{
|
||||||
|
QString s = u.toString();
|
||||||
|
EventContainer hover_event("link-hovered");
|
||||||
|
hover_event["url"] = s;
|
||||||
|
_container->triggerEvent(_view->id(), hover_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebviewWindow::evtRequested(QWebEngineFrame frame)
|
||||||
|
{
|
||||||
|
if (frame.name() == "rkt-evt-frame") {
|
||||||
|
processJsEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void WebviewWindow::moveEvent(QMoveEvent *event)
|
void WebviewWindow::moveEvent(QMoveEvent *event)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QWebEngineCertificateError>
|
#include <QWebEngineCertificateError>
|
||||||
#include <QWebEngineProfile>
|
#include <QWebEngineProfile>
|
||||||
#include <QWebEngineNavigationRequest>
|
#include <QWebEngineNavigationRequest>
|
||||||
|
#include <QWebEngineFrame>
|
||||||
|
|
||||||
class WebViewQt;
|
class WebViewQt;
|
||||||
class Rktwebview_qt;
|
class Rktwebview_qt;
|
||||||
@@ -37,24 +38,28 @@ private:
|
|||||||
|
|
||||||
QWebEngineProfile *_profile;
|
QWebEngineProfile *_profile;
|
||||||
|
|
||||||
|
bool _navigation_event_sent;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleCertificate(const QWebEngineCertificateError &certificateError);
|
void handleCertificate(const QWebEngineCertificateError &certificateError);
|
||||||
void navigationRequested(QWebEngineNavigationRequest &req);
|
void navigationRequested(QWebEngineNavigationRequest &req);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void processJsEvents();
|
void processJsEvents();
|
||||||
|
void closeView();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent *evt);
|
void closeEvent(QCloseEvent *evt);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void closeView();
|
|
||||||
bool windowCreated();
|
bool windowCreated();
|
||||||
int moveCount();
|
int moveCount();
|
||||||
int resizeCount();
|
int resizeCount();
|
||||||
|
|
||||||
void setOUToken(const QString &token);
|
void setOUToken(const QString &token);
|
||||||
|
|
||||||
|
bool navigationEventSent();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void addView(WebViewQt *v, Rktwebview_qt *c);
|
void addView(WebViewQt *v, Rktwebview_qt *c);
|
||||||
WebViewQt *view();
|
WebViewQt *view();
|
||||||
@@ -70,6 +75,8 @@ public:
|
|||||||
private slots:
|
private slots:
|
||||||
void triggerResize();
|
void triggerResize();
|
||||||
void triggerMove();
|
void triggerMove();
|
||||||
|
void linkHovered(const QUrl &u);
|
||||||
|
void evtRequested(QWebEngineFrame frame);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
QWebEngineProfile *profile();
|
QWebEngineProfile *profile();
|
||||||
|
|||||||
447
scrbl/racket-webview-qt.scrbl
Normal file
447
scrbl/racket-webview-qt.scrbl
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
#lang scribble/manual
|
||||||
|
|
||||||
|
@title{Racket FFI Interface for @tt{rktwebview_qt}}
|
||||||
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
|
@section{Overview}
|
||||||
|
|
||||||
|
The module @tt{racket-webview-qt.rkt} provides a Racket FFI wrapper around the
|
||||||
|
native @tt{rktwebview_qt} library. It loads the shared library, initializes the
|
||||||
|
native runtime, and exposes Racket functions for creating and controlling
|
||||||
|
webview windows.
|
||||||
|
|
||||||
|
The wrapper translates the low-level C interface into a Racket-oriented API
|
||||||
|
based on structures, callbacks, and ordinary Racket values.
|
||||||
|
|
||||||
|
The module provides:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{creation of HTTP(S) contexts}
|
||||||
|
@item{creation and management of webview windows}
|
||||||
|
@item{navigation and HTML loading}
|
||||||
|
@item{JavaScript execution}
|
||||||
|
@item{window geometry and visibility control}
|
||||||
|
@item{native dialogs}
|
||||||
|
@item{asynchronous event delivery}
|
||||||
|
@item{version and cleanup utilities}
|
||||||
|
]
|
||||||
|
|
||||||
|
@section{Requirements}
|
||||||
|
|
||||||
|
The native backend requires Qt version @tt{6.10.2} or newer.
|
||||||
|
|
||||||
|
The shared library @tt{rktwebview_qt} must therefore be built against Qt
|
||||||
|
@tt{6.10.2} or a compatible later release.
|
||||||
|
|
||||||
|
Earlier Qt versions are not supported.
|
||||||
|
|
||||||
|
@section{Module Initialization}
|
||||||
|
|
||||||
|
Loading the module performs several initialization steps automatically.
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{determines the operating system and architecture}
|
||||||
|
@item{sets Qt runtime environment variables}
|
||||||
|
@item{loads the @tt{rktwebview_qt} shared library}
|
||||||
|
@item{initializes the native runtime}
|
||||||
|
@item{starts a background thread that processes native events}
|
||||||
|
]
|
||||||
|
|
||||||
|
Currently the wrapper supports the following platforms:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@tt{'linux}}
|
||||||
|
@item{@tt{'windows}}
|
||||||
|
]
|
||||||
|
|
||||||
|
If the current system is unsupported, loading the module raises an error.
|
||||||
|
|
||||||
|
@section{Data Model}
|
||||||
|
|
||||||
|
@subsection{The @tt{rkt-wv} Structure}
|
||||||
|
|
||||||
|
Each webview window is represented by a transparent Racket structure.
|
||||||
|
|
||||||
|
@defstruct*[rkt-wv
|
||||||
|
([win exact-integer?]
|
||||||
|
[evt-queue any/c]
|
||||||
|
[callback procedure?]
|
||||||
|
[valid boolean?]
|
||||||
|
[close-callback procedure?])]{
|
||||||
|
|
||||||
|
Represents a webview instance managed by the Racket wrapper.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@tt{win}: the native integer window handle}
|
||||||
|
@item{@tt{evt-queue}: internal queue of pending event strings}
|
||||||
|
@item{@tt{callback}: user event callback}
|
||||||
|
@item{@tt{valid}: mutable flag indicating whether the wrapper considers the
|
||||||
|
window active}
|
||||||
|
@item{@tt{close-callback}: procedure invoked when the window is closed}
|
||||||
|
]
|
||||||
|
|
||||||
|
Although the structure is transparent, user code should normally treat it as
|
||||||
|
an opaque handle.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-wv-win [wv rkt-wv?]) exact-integer?]{
|
||||||
|
Returns the native window handle associated with @racket[wv].
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{HTTP(S) Contexts}
|
||||||
|
|
||||||
|
A context represents the shared HTTP(S) environment used by webviews.
|
||||||
|
|
||||||
|
Contexts correspond to native WebEngine profiles and determine properties such
|
||||||
|
as:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{cookies and cache}
|
||||||
|
@item{injected JavaScript}
|
||||||
|
@item{trusted certificates}
|
||||||
|
]
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-new-context
|
||||||
|
[boilerplate-js string?]
|
||||||
|
[server-cert bytes?])
|
||||||
|
exact-integer?]{
|
||||||
|
|
||||||
|
Creates a new HTTP(S) context and returns its native identifier.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket[boilerplate-js]: JavaScript source injected into pages}
|
||||||
|
@item{@racket[server-cert]: optional certificate data}
|
||||||
|
]
|
||||||
|
|
||||||
|
The returned context identifier can be passed to
|
||||||
|
@racket[rkt-webview-create] to create webviews within that context.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Creating Webviews}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-create
|
||||||
|
[context exact-integer?]
|
||||||
|
[parent (or/c #f rkt-wv?)]
|
||||||
|
[evt-callback (-> rkt-wv? string? any)]
|
||||||
|
[close-callback (-> any)])
|
||||||
|
rkt-wv?]{
|
||||||
|
|
||||||
|
Creates a new webview window in the given HTTP(S) context.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket[context]: context identifier returned by
|
||||||
|
@racket[rkt-webview-new-context]}
|
||||||
|
@item{@racket[parent]: optional parent webview}
|
||||||
|
@item{@racket[evt-callback]: procedure invoked for each event}
|
||||||
|
@item{@racket[close-callback]: procedure invoked when the window is closed}
|
||||||
|
]
|
||||||
|
|
||||||
|
The result is a new @racket[rkt-wv] structure.
|
||||||
|
|
||||||
|
Events generated by the native layer are delivered asynchronously through
|
||||||
|
@racket[evt-callback].
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Window Lifecycle}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-close [wv rkt-wv?]) boolean?]{
|
||||||
|
|
||||||
|
Requests that the webview window be closed.
|
||||||
|
|
||||||
|
The wrapper forwards the request to the native backend and schedules cleanup of
|
||||||
|
the event-processing loop.
|
||||||
|
|
||||||
|
Returns @racket[#t].
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-valid? [wv rkt-wv?]) boolean?]{
|
||||||
|
|
||||||
|
Returns whether the webview still exists.
|
||||||
|
|
||||||
|
The wrapper first checks its internal validity flag and then queries the native
|
||||||
|
runtime.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-exit) void?]{
|
||||||
|
|
||||||
|
Closes all webviews and stops the background event-processing thread.
|
||||||
|
|
||||||
|
This function is also registered with the Racket plumber so that cleanup occurs
|
||||||
|
automatically when the process exits.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Window Configuration}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-set-title! [wv rkt-wv?] [title string?])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Sets the native window title.
|
||||||
|
|
||||||
|
Returns a result symbol such as:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['oke]}
|
||||||
|
@item{@racket['failed]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-set-ou-token [wv rkt-wv?] [token string?])
|
||||||
|
boolean?]{
|
||||||
|
|
||||||
|
Associates an Organizational Unit token with the window.
|
||||||
|
|
||||||
|
This token may be used by the native layer when accepting certain
|
||||||
|
self-signed certificates.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Navigation}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-set-url! [wv rkt-wv?] [url string?]) symbol?]{
|
||||||
|
|
||||||
|
Navigates the webview to the given URL.
|
||||||
|
|
||||||
|
Returns a result symbol such as:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['oke]}
|
||||||
|
@item{@racket['set_navigate_failed]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-set-html! [wv rkt-wv?] [html string?]) symbol?]{
|
||||||
|
|
||||||
|
Loads HTML directly into the webview.
|
||||||
|
|
||||||
|
Returns a result symbol such as:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['oke]}
|
||||||
|
@item{@racket['set_html_failed]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{JavaScript Execution}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-run-js [wv rkt-wv?] [js string?]) symbol?]{
|
||||||
|
|
||||||
|
Executes JavaScript without returning a value.
|
||||||
|
|
||||||
|
Returns a result symbol such as:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['oke]}
|
||||||
|
@item{@racket['eval_js_failed]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-call-js [wv rkt-wv?] [js string?])
|
||||||
|
(list/c symbol? string?)]{
|
||||||
|
|
||||||
|
Executes JavaScript and returns:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(list result value)
|
||||||
|
]
|
||||||
|
|
||||||
|
where:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket[result] is the native result code}
|
||||||
|
@item{@racket[value] is a JSON string containing the returned data}
|
||||||
|
]
|
||||||
|
|
||||||
|
The JSON structure is generated by the native backend.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Window Geometry}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-move [wv rkt-wv?] [x exact-integer?] [y exact-integer?])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Moves the webview window to the given screen coordinates.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-resize
|
||||||
|
[wv rkt-wv?]
|
||||||
|
[width exact-integer?]
|
||||||
|
[height exact-integer?])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Resizes the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-show [wv rkt-wv?]) symbol?]{
|
||||||
|
Shows the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-hide [wv rkt-wv?]) symbol?]{
|
||||||
|
Hides the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-show-normal [wv rkt-wv?]) symbol?]{
|
||||||
|
Restores the window to its normal state.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-maximize [wv rkt-wv?]) symbol?]{
|
||||||
|
Maximizes the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-minimize [wv rkt-wv?]) symbol?]{
|
||||||
|
Minimizes the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-present [wv rkt-wv?]) symbol?]{
|
||||||
|
Requests that the window be presented to the user.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-window-state [wv rkt-wv?]) symbol?]{
|
||||||
|
|
||||||
|
Returns the current window state.
|
||||||
|
|
||||||
|
Possible results include:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['normal]}
|
||||||
|
@item{@racket['minimized]}
|
||||||
|
@item{@racket['maximized]}
|
||||||
|
@item{@racket['hidden]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Developer Tools}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-open-devtools [wv rkt-wv?]) symbol?]{
|
||||||
|
Opens the browser developer tools window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Native Dialogs}
|
||||||
|
|
||||||
|
Dialog functions return immediately with a status code.
|
||||||
|
The user’s choice is delivered asynchronously through the event callback.
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-choose-dir
|
||||||
|
[wv rkt-wv?]
|
||||||
|
[title string?]
|
||||||
|
[base-dir string?])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Requests a directory-selection dialog.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-file-open
|
||||||
|
[wv rkt-wv?]
|
||||||
|
[title string?]
|
||||||
|
[base-dir string?]
|
||||||
|
[extensions string?])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Requests a file-open dialog.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-file-save
|
||||||
|
[wv rkt-wv?]
|
||||||
|
[title string?]
|
||||||
|
[base-dir string?]
|
||||||
|
[extensions string?])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Requests a file-save dialog.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-messagebox
|
||||||
|
[wv rkt-wv?]
|
||||||
|
[title string?]
|
||||||
|
[message string?]
|
||||||
|
[submessage string?]
|
||||||
|
[type symbol?])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Shows a native message box.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Event Delivery}
|
||||||
|
|
||||||
|
Each webview has an associated event callback.
|
||||||
|
|
||||||
|
The callback has the form:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(λ (wv event-json) ...)
|
||||||
|
]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket[wv]: the webview handle}
|
||||||
|
@item{@racket[event-json]: JSON event string from the native backend}
|
||||||
|
]
|
||||||
|
|
||||||
|
Typical event types include:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@tt{"show"}}
|
||||||
|
@item{@tt{"hide"}}
|
||||||
|
@item{@tt{"move"}}
|
||||||
|
@item{@tt{"resize"}}
|
||||||
|
@item{@tt{"closed"}}
|
||||||
|
@item{@tt{"page-loaded"}}
|
||||||
|
@item{@tt{"navigation-request"}}
|
||||||
|
@item{@tt{"js-evt"}}
|
||||||
|
]
|
||||||
|
|
||||||
|
The wrapper does not parse the JSON payload.
|
||||||
|
|
||||||
|
@section{Version Information}
|
||||||
|
|
||||||
|
@defproc[(rkt-webview-version)
|
||||||
|
(list/c list? list?)]{
|
||||||
|
|
||||||
|
Returns version information for the native backend.
|
||||||
|
|
||||||
|
Example result:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(list
|
||||||
|
(list 'webview-c-api 1 0 0)
|
||||||
|
(list 'qt 6 10 2))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Example}
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(define ctx
|
||||||
|
(rkt-webview-new-context "" #""))
|
||||||
|
|
||||||
|
(define wv
|
||||||
|
(rkt-webview-create
|
||||||
|
ctx
|
||||||
|
#f
|
||||||
|
(λ (wv evt)
|
||||||
|
(displayln evt))
|
||||||
|
(λ ()
|
||||||
|
(displayln "closed"))))
|
||||||
|
|
||||||
|
(rkt-webview-set-title! wv "Example")
|
||||||
|
(rkt-webview-set-url! wv "https://example.org")
|
||||||
|
]
|
||||||
|
|
||||||
|
@section{Summary}
|
||||||
|
|
||||||
|
The FFI module provides a thin Racket interface to the native
|
||||||
|
@tt{rktwebview_qt} backend.
|
||||||
|
|
||||||
|
Key characteristics:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{thin wrapper around the native C API}
|
||||||
|
@item{asynchronous event delivery}
|
||||||
|
@item{JSON-based event payloads}
|
||||||
|
@item{simple Racket structures for webviews}
|
||||||
|
]
|
||||||
797
scrbl/racket-webview.scrbl
Normal file
797
scrbl/racket-webview.scrbl
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
#lang scribble/manual
|
||||||
|
|
||||||
|
@title{High-Level Racket Interface for Webviews}
|
||||||
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
|
@defmodule[racket-webview]
|
||||||
|
|
||||||
|
@section{Overview}
|
||||||
|
|
||||||
|
The module @tt{racket-webview.rkt} provides the high-level Racket interface to
|
||||||
|
the native @tt{rktwebview_qt} backend.
|
||||||
|
|
||||||
|
It is built on top of the low-level FFI layer and adds:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{Racket contracts on exported procedures}
|
||||||
|
@item{HTTP(S) context management}
|
||||||
|
@item{an embedded local HTTPS server per context}
|
||||||
|
@item{automatic self-signed certificate generation}
|
||||||
|
@item{decoded event delivery as Racket hash tables}
|
||||||
|
@item{decoded JavaScript results as Racket values}
|
||||||
|
@item{DOM convenience functions for values, attributes, classes, and styles}
|
||||||
|
]
|
||||||
|
|
||||||
|
Applications are expected to use this module rather than the lower-level FFI
|
||||||
|
module directly.
|
||||||
|
|
||||||
|
@section{Architecture}
|
||||||
|
|
||||||
|
This module sits above the low-level FFI wrapper and provides a more idiomatic
|
||||||
|
Racket API.
|
||||||
|
|
||||||
|
The stack is organized as follows:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{the native Qt backend}
|
||||||
|
@item{the thin FFI wrapper in @tt{racket-webview-qt.rkt}}
|
||||||
|
@item{the higher-level Racket interface in @tt{racket-webview.rkt}}
|
||||||
|
]
|
||||||
|
|
||||||
|
This layer introduces two main abstractions:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@tt{wv-context}, representing an HTTP(S) context}
|
||||||
|
@item{@tt{wv-win}, representing a webview window}
|
||||||
|
]
|
||||||
|
|
||||||
|
@section{HTTP(S) Contexts}
|
||||||
|
|
||||||
|
A context represents the HTTP(S) environment in which webviews operate.
|
||||||
|
|
||||||
|
Unlike the lower-level FFI layer, this module does more than create a native
|
||||||
|
WebEngine profile. It also starts a local HTTPS server bound to
|
||||||
|
@tt{127.0.0.1} on an automatically chosen port.
|
||||||
|
|
||||||
|
Each context therefore combines:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{a native WebEngine context}
|
||||||
|
@item{a local HTTPS server}
|
||||||
|
@item{a self-signed certificate}
|
||||||
|
@item{a base URL}
|
||||||
|
@item{a file getter used to resolve incoming requests}
|
||||||
|
]
|
||||||
|
|
||||||
|
@defstruct*[wv-context
|
||||||
|
([context exact-integer?]
|
||||||
|
[port exact-integer?]
|
||||||
|
[file-getter procedure?]
|
||||||
|
[webserver-thread thread?]
|
||||||
|
[base-url string?]
|
||||||
|
[request-count any/c]
|
||||||
|
[sec-token-cache any/c]
|
||||||
|
[cert-ou-token string?])]{
|
||||||
|
Represents a webview HTTP(S) context.
|
||||||
|
|
||||||
|
The structure is transparent, but it is intended to be treated as a context
|
||||||
|
handle.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-new-context
|
||||||
|
[file-getter procedure?]
|
||||||
|
[#:boilerplate-js boilerplate-js string?
|
||||||
|
(webview-default-boilerplate-js)])
|
||||||
|
wv-context?]{
|
||||||
|
|
||||||
|
Creates a new HTTP(S) context.
|
||||||
|
|
||||||
|
The @racket[file-getter] procedure is used by the embedded web server to map
|
||||||
|
request paths to files on disk.
|
||||||
|
|
||||||
|
The function performs the following steps:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{creates a fresh context structure}
|
||||||
|
@item{generates a self-signed certificate}
|
||||||
|
@item{starts a local HTTPS server on @tt{127.0.0.1}}
|
||||||
|
@item{creates the native context through the FFI layer}
|
||||||
|
@item{stores the resulting base URL}
|
||||||
|
]
|
||||||
|
|
||||||
|
The @racket[boilerplate-js] argument supplies JavaScript that is injected into
|
||||||
|
pages loaded within the native context.
|
||||||
|
|
||||||
|
The returned value is a @racket[wv-context] structure.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(wv-context-base-url [ctx wv-context?]) string?]{
|
||||||
|
Returns the base URL associated with the context.
|
||||||
|
|
||||||
|
This is the local HTTPS URL served by the embedded web server, for example a
|
||||||
|
URL of the form @tt{https://127.0.0.1:port}.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Webview Windows}
|
||||||
|
|
||||||
|
A webview window is represented by a @racket[wv-win] structure.
|
||||||
|
|
||||||
|
@defstruct*[wv-win
|
||||||
|
([handle any/c]
|
||||||
|
[context wv-context?]
|
||||||
|
[window-nr exact-integer?])]{
|
||||||
|
Represents a webview window.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@tt{handle}: the low-level FFI handle}
|
||||||
|
@item{@tt{context}: the owning @racket[wv-context]}
|
||||||
|
@item{@tt{window-nr}: the native numeric window identifier}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(wv-win-window-nr [wv wv-win?]) exact-integer?]{
|
||||||
|
Returns the native numeric window identifier of @racket[wv].
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-create
|
||||||
|
[context wv-context?]
|
||||||
|
[url-path string?]
|
||||||
|
[event-callback procedure?]
|
||||||
|
[#:parent parent (or/c wv-win? #f) #f])
|
||||||
|
wv-win?]{
|
||||||
|
|
||||||
|
Creates a new webview window inside the given context.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket[context]: the HTTP(S) context}
|
||||||
|
@item{@racket[url-path]: initial URL path to load}
|
||||||
|
@item{@racket[event-callback]: callback receiving decoded events}
|
||||||
|
@item{@racket[parent]: optional parent window}
|
||||||
|
]
|
||||||
|
|
||||||
|
The function creates the native window, associates the context certificate
|
||||||
|
OU token with it, and immediately loads the given path through
|
||||||
|
@racket[webview-set-url!].
|
||||||
|
|
||||||
|
The callback receives two arguments:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{the created @racket[wv-win]}
|
||||||
|
@item{a decoded event hash}
|
||||||
|
]
|
||||||
|
|
||||||
|
Native JSON event strings are converted to Racket hashes before delivery.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-close [wv wv-win?]) symbol?]{
|
||||||
|
Closes the webview.
|
||||||
|
|
||||||
|
Returns @racket['oke].
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Navigation and HTML Loading}
|
||||||
|
|
||||||
|
@defproc[(webview-set-url! [wv wv-win?] [url (or/c string? url?)]) symbol?]{
|
||||||
|
|
||||||
|
Loads a URL into the webview.
|
||||||
|
|
||||||
|
If the supplied URL is relative or missing scheme and host information, the
|
||||||
|
function resolves it against the base URL of the window's context.
|
||||||
|
|
||||||
|
This makes it possible to load application pages relative to the embedded local
|
||||||
|
HTTPS server.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-navigate! [wv wv-win?] [place string?]) symbol?]{
|
||||||
|
|
||||||
|
Navigates the current page by assigning to @tt{window.location} through
|
||||||
|
JavaScript.
|
||||||
|
|
||||||
|
This is distinct from @racket[webview-set-url!], which loads a URL through the
|
||||||
|
native navigation interface.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-set-html! [wv wv-win?] [html (or/c string? xexpr?)]) symbol?]{
|
||||||
|
|
||||||
|
Loads HTML directly into the webview.
|
||||||
|
|
||||||
|
If @racket[html] is an x-expression, it is converted with
|
||||||
|
@racket[xexpr->string] first.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-base-url [wv wv-win?]) url?]{
|
||||||
|
Returns the base URL of the webview's context as a Racket URL value.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Window Management}
|
||||||
|
|
||||||
|
@defproc[(webview-devtools [wv wv-win?]) symbol?]{
|
||||||
|
Opens the developer tools for the webview.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-move [wv wv-win?] [x number?] [y number?]) symbol?]{
|
||||||
|
Moves the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-resize [wv wv-win?] [w number?] [h number?]) symbol?]{
|
||||||
|
Resizes the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-show [wv wv-win?]) symbol?]{
|
||||||
|
Shows the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-hide [wv wv-win?]) symbol?]{
|
||||||
|
Hides the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-show-normal [wv wv-win?]) symbol?]{
|
||||||
|
Restores the window to the normal state.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-maximize [wv wv-win?]) symbol?]{
|
||||||
|
Maximizes the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-minimize [wv wv-win?]) symbol?]{
|
||||||
|
Minimizes the window.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-present [wv wv-win?]) symbol?]{
|
||||||
|
Presents the window to the user.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-window-state [wv wv-win?]) symbol?]{
|
||||||
|
Returns the current window state.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-set-title! [wv wv-win?] [title string?]) symbol?]{
|
||||||
|
Sets the native window title.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Message Boxes and File Dialogs}
|
||||||
|
|
||||||
|
@defproc[(webview-messagebox
|
||||||
|
[wv wv-win?]
|
||||||
|
[type symbol?]
|
||||||
|
[title string?]
|
||||||
|
[message string?]
|
||||||
|
[#:sub submessage string? ""])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Shows a native message box.
|
||||||
|
|
||||||
|
The @racket[type] argument is passed through to the lower layer and is expected
|
||||||
|
to be one of the supported message box kinds, such as
|
||||||
|
@racket['info], @racket['error], @racket['warning], @racket['yes-no], or
|
||||||
|
@racket['oke-cancel].
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-choose-dir
|
||||||
|
[wv wv-win?]
|
||||||
|
[title string?]
|
||||||
|
[base-dir (or/c path? string?)])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Opens a directory chooser dialog.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-file-open
|
||||||
|
[wv wv-win?]
|
||||||
|
[title string?]
|
||||||
|
[base-dir (or/c path? string?)]
|
||||||
|
[permitted-exts (or/c wv-permitted-exts? wv-list-of-permitted-exts?)])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Opens a native file-open dialog.
|
||||||
|
|
||||||
|
The @racket[permitted-exts] value is converted to a Qt file filter string.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-file-save
|
||||||
|
[wv wv-win?]
|
||||||
|
[title string?]
|
||||||
|
[base-dir (or/c path? string?)]
|
||||||
|
[permitted-exts (or/c wv-permitted-exts? wv-list-of-permitted-exts?)])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Opens a native file-save dialog.
|
||||||
|
|
||||||
|
The @racket[permitted-exts] value is converted to a Qt file filter string.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Permitted Extensions}
|
||||||
|
|
||||||
|
The file dialog helpers use @racket[wv-permitted-exts] values to describe file
|
||||||
|
type filters.
|
||||||
|
|
||||||
|
@defstruct*[wv-permitted-exts
|
||||||
|
([name string?]
|
||||||
|
[exts wv-permitted-exts-exts?])]{
|
||||||
|
Represents a named file filter.
|
||||||
|
|
||||||
|
The @racket[name] field is the human-readable filter name.
|
||||||
|
|
||||||
|
The @racket[exts] field is a list of filename extensions represented as
|
||||||
|
symbols.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(wv-permitted-exts-exts? [v any/c]) boolean?]{
|
||||||
|
Returns whether @racket[v] is a valid list of extension symbols.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(wv-permitted-exts-name? [v any/c]) boolean?]{
|
||||||
|
Returns whether @racket[v] is a valid filter name.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(wv-list-of-permitted-exts? [v any/c]) boolean?]{
|
||||||
|
Returns whether @racket[v] is a list of valid @racket[wv-permitted-exts]
|
||||||
|
values.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-filter->exts [str string?])
|
||||||
|
(or/c wv-permitted-exts? #f)]{
|
||||||
|
Parses a Qt-style filter string into a @racket[wv-permitted-exts] value.
|
||||||
|
|
||||||
|
If the string does not match the expected filter format, the result is
|
||||||
|
@racket[#f].
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Event Binding}
|
||||||
|
|
||||||
|
The module provides helpers for binding DOM events from page elements back to
|
||||||
|
the Racket side.
|
||||||
|
|
||||||
|
@defproc[(webview-bind!
|
||||||
|
[wv wv-win?]
|
||||||
|
[selector (or/c symbol? string?)]
|
||||||
|
[event (or/c symbol? list-of-symbol?)])
|
||||||
|
list?]{
|
||||||
|
|
||||||
|
Binds one or more DOM events to elements selected by @racket[selector].
|
||||||
|
|
||||||
|
If @racket[selector] is a symbol, it is interpreted as an element id and
|
||||||
|
rewritten as a CSS id selector.
|
||||||
|
|
||||||
|
If @racket[event] is a symbol, it is treated as a single event name.
|
||||||
|
|
||||||
|
The result is a list describing the created bindings.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-unbind!
|
||||||
|
[wv wv-win?]
|
||||||
|
[selector (or/c symbol? string?)]
|
||||||
|
[event (or/c symbol? list-of-symbol?)])
|
||||||
|
list?]{
|
||||||
|
|
||||||
|
Removes previously established DOM event bindings.
|
||||||
|
|
||||||
|
Its arguments and return value follow the same conventions as
|
||||||
|
@racket[webview-bind!].
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{JavaScript Execution}
|
||||||
|
|
||||||
|
@defproc[(webview-run-js [wv wv-win?] [js string?]) symbol?]{
|
||||||
|
Executes JavaScript for side effects.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-call-js-result? [x any/c]) boolean?]{
|
||||||
|
Returns whether @racket[x] has the low-level two-element result shape used by
|
||||||
|
the FFI function @racket[rkt-webview-call-js].
|
||||||
|
|
||||||
|
This predicate is mainly a utility for internal result checking.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-call-js [wv wv-win?] [js string?])
|
||||||
|
(or/c string? list? boolean? hash?)]{
|
||||||
|
|
||||||
|
Executes JavaScript and returns the decoded @tt{result} field of the underlying
|
||||||
|
native JSON response.
|
||||||
|
|
||||||
|
Unlike the lower-level FFI wrapper, this function does not return a
|
||||||
|
@racket[(list result-symbol json-string)] pair. Instead:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{the low-level result is checked}
|
||||||
|
@item{the JSON string is decoded}
|
||||||
|
@item{the @tt{result} field is extracted}
|
||||||
|
@item{the extracted value is returned directly}
|
||||||
|
]
|
||||||
|
|
||||||
|
If the JavaScript evaluation fails, the function raises an error.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{DOM Content Helpers}
|
||||||
|
|
||||||
|
@defproc[(webview-set-innerHTML!
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[html (or/c string? xexpr?)])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Sets the @tt{innerHTML} of the element with the given id.
|
||||||
|
|
||||||
|
If @racket[html] is an x-expression, it is converted to HTML first.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Form Value Helpers}
|
||||||
|
|
||||||
|
@defproc[(webview-set-value!
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[val (or/c symbol?
|
||||||
|
string?
|
||||||
|
number?
|
||||||
|
boolean?
|
||||||
|
g:date?
|
||||||
|
g:time?
|
||||||
|
g:datetime?
|
||||||
|
rgba?)])
|
||||||
|
symbol?]{
|
||||||
|
|
||||||
|
Sets the value of the element with the given id.
|
||||||
|
|
||||||
|
Checkboxes and radio buttons are handled through their @tt{checked} property.
|
||||||
|
Other elements are handled through their @tt{value} property.
|
||||||
|
|
||||||
|
Date, time, datetime, and color values are converted to strings before being
|
||||||
|
sent to the browser.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value [wv wv-win?] [id symbol?])
|
||||||
|
(or/c string? boolean?)]{
|
||||||
|
|
||||||
|
Returns the current value of the element with the given id.
|
||||||
|
|
||||||
|
For checkboxes and radio buttons, the returned value reflects the checked state.
|
||||||
|
For other elements, the result is the string value.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value/boolean [wv wv-win?] [id symbol?])
|
||||||
|
(or/c boolean? #f)]{
|
||||||
|
Returns the current value converted to a boolean.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value/symbol [wv wv-win?] [id symbol?])
|
||||||
|
(or/c symbol? #f)]{
|
||||||
|
Returns the current value converted to a symbol.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value/number [wv wv-win?] [id symbol?])
|
||||||
|
(or/c number? #f)]{
|
||||||
|
Returns the current value converted to a number.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value/date [wv wv-win?] [id symbol?])
|
||||||
|
(or/c g:date? #f)]{
|
||||||
|
Returns the current value converted to a date.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value/time [wv wv-win?] [id symbol?])
|
||||||
|
(or/c g:time? #f)]{
|
||||||
|
Returns the current value converted to a time.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value/datetime [wv wv-win?] [id symbol?])
|
||||||
|
(or/c g:datetime? #f)]{
|
||||||
|
Returns the current value converted to a datetime.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-value/color [wv wv-win?] [id symbol?])
|
||||||
|
(or/c rgba? #f)]{
|
||||||
|
Returns the current value converted to an RGBA color.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Class Helpers}
|
||||||
|
|
||||||
|
@defproc[(webview-add-class!
|
||||||
|
[wv wv-win?]
|
||||||
|
[id-or-selector (or/c symbol? string?)]
|
||||||
|
[class (or/c symbol? string? list?)])
|
||||||
|
hash?]{
|
||||||
|
|
||||||
|
Adds one or more CSS classes to all elements selected by
|
||||||
|
@racket[id-or-selector].
|
||||||
|
|
||||||
|
A symbol selector is interpreted as an element id.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-remove-class!
|
||||||
|
[wv wv-win?]
|
||||||
|
[id-or-selector (or/c symbol? string?)]
|
||||||
|
[class (or/c symbol? string? list?)])
|
||||||
|
hash?]{
|
||||||
|
|
||||||
|
Removes one or more CSS classes from all selected elements.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Style Helpers}
|
||||||
|
|
||||||
|
@defproc[(webview-set-style!
|
||||||
|
[wv wv-win?]
|
||||||
|
[selector (or/c symbol? string?)]
|
||||||
|
[style-entries (or/c kv? list-of-kv?)])
|
||||||
|
hash?]{
|
||||||
|
|
||||||
|
Sets one or more inline CSS style properties on the selected elements.
|
||||||
|
|
||||||
|
The @racket[style-entries] argument is either a single key/value pair or a list
|
||||||
|
of key/value pairs.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-get-style
|
||||||
|
[wv wv-win?]
|
||||||
|
[selector (or/c symbol? string?)]
|
||||||
|
[styles (or/c symbol? list-of-symbol?)])
|
||||||
|
(or/c list? hash?)]{
|
||||||
|
|
||||||
|
Reads computed style values from the selected elements.
|
||||||
|
|
||||||
|
If @racket[selector] is a symbol, the result is the style hash for that single
|
||||||
|
element.
|
||||||
|
|
||||||
|
If @racket[selector] is a string selector, the result is a list of
|
||||||
|
@racket[(cons id style-hash)] pairs.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-unset-style!
|
||||||
|
[wv wv-win?]
|
||||||
|
[selector (or/c symbol? string?)]
|
||||||
|
[style-entries (or/c symbol? list-of-symbol?)])
|
||||||
|
hash?]{
|
||||||
|
|
||||||
|
Clears one or more inline style properties from the selected elements.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Attribute Helpers}
|
||||||
|
|
||||||
|
@defproc[(webview-set-attr!
|
||||||
|
[wv wv-win?]
|
||||||
|
[selector (or/c symbol? string?)]
|
||||||
|
[attr-entries (or/c kv? list-of-kv?)])
|
||||||
|
hash?]{
|
||||||
|
|
||||||
|
Sets one or more HTML attributes on the selected elements.
|
||||||
|
|
||||||
|
Date, time, datetime, and color values are converted to strings before being
|
||||||
|
sent to the browser.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c string? boolean?)]{
|
||||||
|
|
||||||
|
Returns the value of the named attribute of the element with the given id.
|
||||||
|
|
||||||
|
If the attribute does not exist, the result is @racket[#f].
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr/boolean
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c boolean? #f)]{
|
||||||
|
Returns the attribute converted to a boolean.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr/symbol
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c symbol? #f)]{
|
||||||
|
Returns the attribute converted to a symbol.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr/number
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c number? #f)]{
|
||||||
|
Returns the attribute converted to a number.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr/date
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c g:date? #f)]{
|
||||||
|
Returns the attribute converted to a date.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr/time
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c g:time? #f)]{
|
||||||
|
Returns the attribute converted to a time.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr/datetime
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c g:datetime? #f)]{
|
||||||
|
Returns the attribute converted to a datetime.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-attr/color
|
||||||
|
[wv wv-win?]
|
||||||
|
[id symbol?]
|
||||||
|
[attr (or/c symbol? string?)])
|
||||||
|
(or/c rgba? #f)]{
|
||||||
|
Returns the attribute converted to an RGBA color.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Utility Functions}
|
||||||
|
|
||||||
|
@defproc[(webview-standard-file-getter
|
||||||
|
[base-path path-string?]
|
||||||
|
[#:not-exist on-not-exist procedure?
|
||||||
|
(λ (file base-path path) path)])
|
||||||
|
procedure?]{
|
||||||
|
|
||||||
|
Creates a standard file getter for use with @racket[webview-new-context].
|
||||||
|
|
||||||
|
The resulting procedure maps request paths to files under @racket[base-path].
|
||||||
|
|
||||||
|
The special request path @tt{/} is mapped to @tt{index.html}.
|
||||||
|
|
||||||
|
If the resolved file does not exist, the @racket[on-not-exist] procedure is
|
||||||
|
used to determine the result path.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-default-boilerplate-js [custom-js procedure?] ...)
|
||||||
|
string?]{
|
||||||
|
|
||||||
|
Returns the default boilerplate JavaScript used by the system.
|
||||||
|
|
||||||
|
Any supplied procedures are called and their returned JavaScript strings are
|
||||||
|
appended to the default boilerplate.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defproc[(webview-version) pair?]{
|
||||||
|
|
||||||
|
Returns version information for the high-level webview package and the native
|
||||||
|
backend.
|
||||||
|
|
||||||
|
The result is a pair whose first element describes the Racket webview package
|
||||||
|
version and whose remaining data comes from @racket[rkt-webview-version].
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Event Reference}
|
||||||
|
|
||||||
|
Events are delivered to @racket[webview-create]'s callback as decoded Racket
|
||||||
|
hash tables.
|
||||||
|
|
||||||
|
The helper @racket[util-parse-event] converts the incoming JSON and also
|
||||||
|
normalizes the @tt{event} field from a string to a symbol.
|
||||||
|
|
||||||
|
Each event therefore contains at least an @racket['event] entry.
|
||||||
|
|
||||||
|
@subsection{Window Events}
|
||||||
|
|
||||||
|
@subsubsection{@tt{show}}
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
'#hash((event . show))
|
||||||
|
]
|
||||||
|
|
||||||
|
@subsubsection{@tt{hide}}
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
'#hash((event . hide))
|
||||||
|
]
|
||||||
|
|
||||||
|
@subsubsection{@tt{move}}
|
||||||
|
|
||||||
|
Typical fields:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['x]}
|
||||||
|
@item{@racket['y]}
|
||||||
|
]
|
||||||
|
|
||||||
|
@subsubsection{@tt{resize}}
|
||||||
|
|
||||||
|
Typical fields:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['w]}
|
||||||
|
@item{@racket['h]}
|
||||||
|
]
|
||||||
|
|
||||||
|
@subsubsection{@tt{closed}}
|
||||||
|
|
||||||
|
Indicates that the window was closed.
|
||||||
|
|
||||||
|
@subsubsection{@tt{can-close?}}
|
||||||
|
|
||||||
|
Generated when the user attempts to close the window.
|
||||||
|
|
||||||
|
@subsection{Navigation and Loading Events}
|
||||||
|
|
||||||
|
@subsubsection{@tt{page-loaded}}
|
||||||
|
|
||||||
|
Contains an @racket['oke] field indicating whether loading succeeded.
|
||||||
|
|
||||||
|
@subsubsection{@tt{navigation-request}}
|
||||||
|
|
||||||
|
Typically contains:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['url]}
|
||||||
|
@item{@racket['type]}
|
||||||
|
]
|
||||||
|
|
||||||
|
@subsection{JavaScript Events}
|
||||||
|
|
||||||
|
@subsubsection{@tt{js-evt}}
|
||||||
|
|
||||||
|
Represents a JavaScript-originated event sent through the injected browser
|
||||||
|
bridge.
|
||||||
|
|
||||||
|
The original JavaScript payload is available under the @racket['js-evt] key.
|
||||||
|
|
||||||
|
@subsection{Dialog Events}
|
||||||
|
|
||||||
|
Dialog completion is reported through ordinary events, including event names
|
||||||
|
such as:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@tt{choose-dir-ok}}
|
||||||
|
@item{@tt{choose-dir-cancel}}
|
||||||
|
@item{@tt{file-open-ok}}
|
||||||
|
@item{@tt{file-open-cancel}}
|
||||||
|
@item{@tt{file-save-ok}}
|
||||||
|
@item{@tt{file-save-cancel}}
|
||||||
|
@item{@tt{msgbox-ok}}
|
||||||
|
@item{@tt{msgbox-cancel}}
|
||||||
|
@item{@tt{msgbox-yes}}
|
||||||
|
@item{@tt{msgbox-no}}
|
||||||
|
]
|
||||||
|
|
||||||
|
@section{Example}
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(define file-getter
|
||||||
|
(webview-standard-file-getter "htdocs"))
|
||||||
|
|
||||||
|
(define ctx
|
||||||
|
(webview-new-context file-getter))
|
||||||
|
|
||||||
|
(define wv
|
||||||
|
(webview-create
|
||||||
|
ctx
|
||||||
|
"index.html"
|
||||||
|
(λ (wv evt)
|
||||||
|
(displayln evt))))
|
||||||
|
|
||||||
|
(webview-set-title! wv "Example")
|
||||||
|
(webview-resize wv 800 600)
|
||||||
|
]
|
||||||
|
|
||||||
|
@section{Summary}
|
||||||
|
|
||||||
|
The module @tt{racket-webview.rkt} provides the main Racket programming
|
||||||
|
interface for the webview system.
|
||||||
|
|
||||||
|
Compared to the lower-level FFI layer, it adds:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{HTTP(S) contexts with local web serving}
|
||||||
|
@item{automatic certificate generation}
|
||||||
|
@item{decoded event delivery}
|
||||||
|
@item{decoded JavaScript results}
|
||||||
|
@item{DOM convenience helpers}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,308 +1,346 @@
|
|||||||
#lang scribble/manual
|
#lang scribble/manual
|
||||||
|
|
||||||
@title{Internal Architecture and Behavior of the Qt–Backed WebView Bridge}
|
@title{Structure and behavior of @tt{rktwebview_qt}}
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
@italic{Technical architecture notes for maintainers}
|
@section{Overview}
|
||||||
|
|
||||||
|
@tt{rktwebview_qt} is a Qt-based backend that provides embedded webviews for
|
||||||
|
applications written in Racket. The library exposes a small C-compatible API
|
||||||
|
intended to be used through the Racket FFI. Internally, the implementation is
|
||||||
|
written in C++ and uses Qt WebEngine.
|
||||||
|
|
||||||
|
The backend provides:
|
||||||
@;@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
|
@itemlist[#:style 'compact
|
||||||
@item{All UI work must execute on the Qt GUI thread; the host may call from any thread.}
|
@item{creation of browser windows}
|
||||||
@item{The host experiences a synchronous API; internally, work is dispatched asynchronously and awaited by pumping the event loop in bounded slices.}
|
@item{navigation to URLs or HTML content}
|
||||||
@item{A minimal JSON bridge enables page→host messages without exposing Qt types over the boundary.}
|
@item{execution of JavaScript}
|
||||||
@item{Window lifecycle, geometry, dialogs, and developer tools behave deterministically and are observable via structured events.}
|
@item{return values from JavaScript}
|
||||||
|
@item{delivery of browser and window events}
|
||||||
|
@item{native dialogs such as file choosers and message boxes}
|
||||||
]
|
]
|
||||||
|
|
||||||
@section{Core Building Blocks}
|
The implementation is intentionally organized so that the external interface
|
||||||
|
remains simple and language-neutral while the Qt-specific logic is hidden
|
||||||
|
inside the runtime controller.
|
||||||
|
|
||||||
@subsection{Command and CommandEvent}
|
@section{Requirements}
|
||||||
|
|
||||||
|
The @tt{rktwebview_qt} backend requires Qt version @tt{6.10.2} or newer.
|
||||||
|
|
||||||
|
The backend is built against Qt WebEngine and depends on the runtime
|
||||||
|
components provided by Qt. Earlier Qt versions are not supported.
|
||||||
|
|
||||||
|
Applications using the backend must therefore ensure that a compatible
|
||||||
|
Qt runtime (≥ @tt{6.10.2}) is available.
|
||||||
|
|
||||||
|
In packaged builds the required Qt runtime is normally bundled together
|
||||||
|
with the backend library.
|
||||||
|
|
||||||
|
@subsection{Compatibility}
|
||||||
|
|
||||||
|
The backend requires Qt version @tt{6.10.2} or newer.
|
||||||
|
|
||||||
|
This requirement is due to the use of the method
|
||||||
|
@tt{setAdditionalTrustedCertificates} provided by
|
||||||
|
@tt{QWebEngineProfileBuilder}. This functionality allows the backend to
|
||||||
|
install additional trusted certificates for a context.
|
||||||
|
|
||||||
|
The mechanism is used to support trusted self-signed certificates for
|
||||||
|
local services. Earlier Qt versions do not provide this functionality
|
||||||
|
and are therefore not supported.
|
||||||
|
|
||||||
|
Packaged builds typically include the required Qt runtime components.
|
||||||
|
|
||||||
|
@section{Layers}
|
||||||
|
|
||||||
|
The system consists of several layers.
|
||||||
|
|
||||||
|
@subsection{C ABI Layer}
|
||||||
|
|
||||||
|
The file @tt{rktwebview.h} defines the public API. This layer provides a stable
|
||||||
|
C-compatible interface that can easily be consumed from Racket through FFI.
|
||||||
|
|
||||||
|
Important characteristics of the ABI include:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@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{numeric handles represent windows and browser contexts}
|
||||||
@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{result codes indicate success or failure}
|
||||||
@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}.}
|
@item{data returned from the library is represented as @tt{rkt_data_t}}
|
||||||
|
@item{event callbacks deliver JSON strings describing events}
|
||||||
]
|
]
|
||||||
|
|
||||||
@subsection{@tt{Rktwebview_qt} Controller}
|
The C layer is implemented in @tt{rktwebview.cpp}. These functions mainly
|
||||||
|
forward calls to a singleton instance of the internal runtime class
|
||||||
|
@tt{Rktwebview_qt}.
|
||||||
|
|
||||||
|
@section{Runtime Controller}
|
||||||
|
|
||||||
|
The class @tt{Rktwebview_qt} acts as the central runtime controller.
|
||||||
|
|
||||||
|
Its responsibilities include:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{Owns the singleton @tt{QApplication} and initializes the event-pump timers.}
|
@item{owning the Qt application instance}
|
||||||
@item{Allocates integer window handles and maintains:
|
@item{managing https contexts}
|
||||||
@itemlist[#:style 'compact
|
@item{managing open windows}
|
||||||
@item{@tt{_views}: handle → @tt{WebviewWindow*}}
|
@item{dispatching commands}
|
||||||
@item{@tt{_view_js_callbacks}: handle → host event callback}
|
@item{forwarding events to the host application}
|
||||||
]
|
|
||||||
}
|
|
||||||
@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)}
|
Internally, it maintains several registries:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@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{a map from window handles to @tt{WebviewWindow} instances}
|
||||||
@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.}
|
@item{a map from context identifiers to @tt{QWebEngineProfile} objects}
|
||||||
|
@item{a map from window handles to callback functions}
|
||||||
]
|
]
|
||||||
|
|
||||||
@subsection{Utility Layer}
|
Each API call that manipulates a window or browser state is forwarded through
|
||||||
|
this controller.
|
||||||
|
|
||||||
|
@section{HTTP(s) Contexts}
|
||||||
|
|
||||||
|
A http(s) context represents a shared WebEngine profile.
|
||||||
|
|
||||||
|
Contexts are created using @tt{rkt_webview_new_context}. Each context contains:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{@tt{EventContainer}: a lightweight key–value structure for assembling outbound events.}
|
@item{a @tt{QWebEngineProfile}}
|
||||||
@item{@tt{mkEventJson}: serializes an @tt{EventContainer} to compact JSON text for the host callback.}
|
@item{optional injected JavaScript code}
|
||||||
|
@item{optional trusted certificates}
|
||||||
]
|
]
|
||||||
|
|
||||||
@section{Threading, Event Loop, and Timing}
|
Multiple windows normally share the same context. This allows cookies,
|
||||||
|
configuration, and injected scripts to be reused.
|
||||||
|
|
||||||
@subsection{Initialization and Timers}
|
A context normally represents a https server that can be reached on a certain (local) Url.
|
||||||
|
|
||||||
On the first entry (or via explicit initialization), the controller constructs
|
@section{Window and View Classes}
|
||||||
the @tt{QApplication} and sets up two key timers:
|
|
||||||
|
Two Qt classes implement the actual browser window.
|
||||||
|
|
||||||
|
@subsection{@tt{WebviewWindow}}
|
||||||
|
|
||||||
|
@tt{WebviewWindow} derives from @tt{QMainWindow}. It represents the native
|
||||||
|
top-level window.
|
||||||
|
|
||||||
|
Responsibilities include:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@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{hosting the browser widget}
|
||||||
@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.}
|
@item{reporting window events such as show, hide, move, and resize}
|
||||||
|
@item{handling close requests}
|
||||||
|
@item{reporting navigation events}
|
||||||
|
@item{forwarding JavaScript events}
|
||||||
]
|
]
|
||||||
|
|
||||||
@subsection{Bounded Event-Loop Pumping}
|
Window geometry changes are slightly delayed through timers so that the host
|
||||||
|
application receives fewer intermediate events.
|
||||||
|
|
||||||
Because the host may not yield a classic Qt main loop, the controller exposes a
|
@subsection{@tt{WebViewQt}}
|
||||||
“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:
|
@tt{WebViewQt} derives from @tt{QWebEngineView}. This class mainly associates
|
||||||
|
the Qt widget with a numeric handle used by the external API.
|
||||||
|
|
||||||
|
@section{Command Dispatch}
|
||||||
|
|
||||||
|
Many API functions are implemented using a small command-dispatch mechanism.
|
||||||
|
|
||||||
|
A command object represents a pending request. The request is delivered to the
|
||||||
|
Qt event system using a custom Qt event. The runtime controller receives the
|
||||||
|
event and executes the corresponding operation.
|
||||||
|
|
||||||
|
This design ensures that GUI operations occur inside the Qt event loop while
|
||||||
|
the external API remains synchronous.
|
||||||
|
|
||||||
|
@section{Event Processing}
|
||||||
|
|
||||||
|
Qt events are processed through repeated calls to
|
||||||
|
@tt{rkt_webview_process_events}, which internally calls
|
||||||
|
@tt{QApplication::processEvents}. This allows the host runtime to control how
|
||||||
|
frequently GUI events are processed.
|
||||||
|
|
||||||
|
@section{Navigation}
|
||||||
|
|
||||||
|
Two main navigation operations are provided.
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{Deliver posted @tt{CommandEvent}s to the GUI thread.}
|
@item{@tt{rkt_webview_set_url} loads a URL}
|
||||||
@item{Complete asynchronous WebEngine operations (e.g., JS callbacks, load signals).}
|
@item{@tt{rkt_webview_set_html} loads raw HTML}
|
||||||
@item{Run the JS harvester that drains page→host events.}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
This design preserves GUI responsiveness and enables synchronous host calls
|
When page loading finishes, a @tt{"page-loaded"} event is emitted. The event
|
||||||
without blocking the GUI thread.
|
contains a boolean field indicating whether loading succeeded.
|
||||||
|
|
||||||
@section{Lifecycle and Window Semantics}
|
@section{JavaScript Execution}
|
||||||
|
|
||||||
@subsection{Creation and Parenting}
|
JavaScript execution is provided through two functions.
|
||||||
|
|
||||||
|
@subsection{Fire-and-Forget Execution}
|
||||||
|
|
||||||
|
@tt{rkt_webview_run_js} executes JavaScript without returning a result.
|
||||||
|
|
||||||
|
This is useful for DOM manipulation or triggering page behavior.
|
||||||
|
|
||||||
|
@subsection{Synchronous Execution with Result}
|
||||||
|
|
||||||
|
@tt{rkt_webview_call_js} evaluates JavaScript and returns a result object.
|
||||||
|
|
||||||
|
The JavaScript is wrapped in a small function that captures either the result
|
||||||
|
value or an exception. The result is returned as a JSON object containing:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{A “create” command constructs a @tt{WebviewWindow}, optionally modal if a valid parent handle is supplied.}
|
@item{@tt{oke}}
|
||||||
@item{A @tt{WebViewQt} is created with a fresh handle and set as the central widget.}
|
@item{@tt{result}}
|
||||||
@item{The window is shown; the command returns only after the window reports @emph{created}, ensuring safe subsequent operations.}
|
@item{@tt{exn}}
|
||||||
]
|
]
|
||||||
|
|
||||||
@subsection{Closing and the Blocked-Close Pattern}
|
The native side waits for the asynchronous Qt callback before returning the
|
||||||
|
value to the host application.
|
||||||
|
|
||||||
|
@section{JavaScript Event Bridge}
|
||||||
|
|
||||||
|
Communication from JavaScript to the host application uses a small event queue
|
||||||
|
implemented in injected JavaScript.
|
||||||
|
|
||||||
|
When a browser context is created, the runtime injects support code defining
|
||||||
|
the following objects:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@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{@tt{window.rkt_event_queue}}
|
||||||
@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.}
|
@item{@tt{window.rkt_send_event(obj)}}
|
||||||
|
@item{@tt{window.rkt_get_events()}}
|
||||||
]
|
]
|
||||||
|
|
||||||
@subsection{Visibility and Activation}
|
The queue stores JavaScript-generated events until the native side retrieves
|
||||||
|
them.
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@subsection{Event Queue}
|
||||||
@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}
|
When page code wants to emit an event, it calls:
|
||||||
|
|
||||||
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|{
|
@verbatim|{
|
||||||
window.rkt_event_queue = [];
|
window.rkt_send_event(obj)
|
||||||
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:
|
The object is appended to the queue.
|
||||||
|
|
||||||
|
The queue can be retrieved using:
|
||||||
|
|
||||||
|
@verbatim|{
|
||||||
|
window.rkt_get_events()
|
||||||
|
}|
|
||||||
|
|
||||||
|
This function returns a JSON array and clears the queue.
|
||||||
|
|
||||||
|
@subsection{Native Notification}
|
||||||
|
|
||||||
|
To notify the native side that events are waiting, the injected code creates a
|
||||||
|
hidden iframe named @tt{rkt-evt-frame}. The iframe is used only as a signalling
|
||||||
|
mechanism.
|
||||||
|
|
||||||
|
After adding an event to the queue, the script attempts to call:
|
||||||
|
|
||||||
|
@verbatim|{
|
||||||
|
frameWindow.print()
|
||||||
|
}|
|
||||||
|
|
||||||
|
Qt receives this through the signal
|
||||||
|
@tt{QWebEnginePage::printRequestedByFrame}.
|
||||||
|
|
||||||
|
The window implementation checks whether the frame name is
|
||||||
|
@tt{"rkt-evt-frame"}. If so, it calls @tt{processJsEvents} to retrieve the
|
||||||
|
queued events.
|
||||||
|
|
||||||
|
This results in the following sequence:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{The helpers are injected after page load so they are present for app code regardless of the navigated content.}
|
@item{JavaScript calls @tt{rkt_send_event}.}
|
||||||
@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.}
|
@item{The event is added to the queue.}
|
||||||
|
@item{The hidden iframe triggers a print request.}
|
||||||
|
@item{Qt receives the request.}
|
||||||
|
@item{The native side calls @tt{rkt_get_events}.}
|
||||||
|
@item{Each event object is delivered to the host.}
|
||||||
]
|
]
|
||||||
|
|
||||||
@subsection{Periodic Harvesting (Page → Host)}
|
The mechanism is therefore not a simple native polling loop. It is better
|
||||||
|
described as a queued JavaScript event bridge with a lightweight native
|
||||||
|
wake-up signal.
|
||||||
|
|
||||||
A fast timer iterates active windows and evaluates @tt{rkt_get_events()} in each
|
@subsection{Delivered Event Format}
|
||||||
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
|
JavaScript-originated events are forwarded to the host as structured JSON.
|
||||||
@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)}
|
Each event uses a generic envelope:
|
||||||
|
|
||||||
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|{
|
@verbatim|{
|
||||||
{
|
{
|
||||||
"oke": <boolean>,
|
"evt": "js-evt",
|
||||||
"result": <any-or-false>,
|
"js-evt": { ... }
|
||||||
"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}
|
The inner object is the original JavaScript event payload.
|
||||||
|
|
||||||
@subsection{Native Dialogs}
|
This design keeps the native API simple while allowing arbitrary JavaScript
|
||||||
|
objects to be delivered to the host application.
|
||||||
|
|
||||||
|
@section{Window Lifecycle}
|
||||||
|
|
||||||
|
Windows are created using @tt{rkt_webview_create}. Each window receives a
|
||||||
|
callback function used to deliver events.
|
||||||
|
|
||||||
|
Important lifecycle events include:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@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{"show"}}
|
||||||
@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{@tt{"hide"}}
|
||||||
@item{The host must free returned result objects; see @secref{memory}.}
|
@item{@tt{"move"}}
|
||||||
|
@item{@tt{"resize"}}
|
||||||
|
@item{@tt{"closed"}}
|
||||||
]
|
]
|
||||||
|
|
||||||
@subsection{Certificate Handling with OU Token}
|
If the user attempts to close a window directly, the close request is converted
|
||||||
|
into a @tt{"can-close?"} event so that the host application can decide whether
|
||||||
|
the window should actually close.
|
||||||
|
|
||||||
When a certificate error arises, the window inspects the chain; if it encounters
|
@section{Native Dialogs}
|
||||||
a self-signed certificate whose Organizational Unit (OU) equals the configured
|
|
||||||
token, it accepts the certificate.
|
The backend also exposes native dialogs including:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{This provides a controlled trust mechanism for private/dev deployments.}
|
@item{directory selection}
|
||||||
@item{Ordinary sites remain subject to standard verification.}
|
@item{file open}
|
||||||
|
@item{file save}
|
||||||
|
@item{message boxes}
|
||||||
]
|
]
|
||||||
|
|
||||||
@section{Event Model (Delivered to Host)}
|
The results of these dialogs are delivered as events so that the host
|
||||||
|
application remains in control of the user interface flow.
|
||||||
|
|
||||||
All events are delivered as compact JSON strings to the host’s callback.
|
@section{Certificates}
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
Contexts may optionally install trusted certificates. Additionally, a window
|
||||||
@item{@tt{"page-loaded"}: emitted after helper JS installation; includes an @tt{"oke"} boolean indicating load success.}
|
may specify a special Organizational Unit token used to automatically trust
|
||||||
@item{@tt{"show"} and @tt{"hide"}: visibility transitions (hide is suppressed on final close).}
|
certain self-signed certificates.
|
||||||
@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 mechanism is useful when communicating with local development services.
|
||||||
|
|
||||||
This section summarizes each command’s core behavior and completion rule.
|
@section{Memory Ownership}
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
Data returned from the library and (javascript) events originating from the library
|
||||||
@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.}
|
are allocated on the native side and must be released by the host using
|
||||||
@item{@bold{CLOSE}: mark as allowed to close, call @tt{close()}, emit @tt{"closed"} on destruction, unregister handle.}
|
@tt{rkt_webview_free_data}.
|
||||||
@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}
|
This avoids exposing C++ ownership semantics across the FFI boundary.
|
||||||
@;@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}
|
@section{Summary}
|
||||||
|
|
||||||
Internally, this bridge is a command-queued, GUI-thread–safe façade over Qt
|
The @tt{rktwebview_qt} backend provides a compact architecture for embedding
|
||||||
WebEngine. The host sees synchronous operations; under the hood, each action is
|
Qt WebEngine inside Racket applications.
|
||||||
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
|
The design allows Racket programs to implement desktop applications using
|
||||||
place. A small JSON bridge carries page→host messages and supports synchronous
|
HTML and JavaScript for the user interface while keeping application logic in
|
||||||
host→page calls with structured results, keeping the boundary stable and
|
the host runtime.
|
||||||
language-neutral.
|
|
||||||
121
scrbl/wv-settings.scrbl
Normal file
121
scrbl/wv-settings.scrbl
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#lang scribble/manual
|
||||||
|
|
||||||
|
@(require scribble/racket
|
||||||
|
scribble/example
|
||||||
|
scribble/core
|
||||||
|
(for-label racket/base
|
||||||
|
racket/string
|
||||||
|
racket/class
|
||||||
|
racket/file)
|
||||||
|
)
|
||||||
|
|
||||||
|
@defmodule[wv-settings]
|
||||||
|
|
||||||
|
@title{Object-Oriented Interface: @racket[wv-settings%]}
|
||||||
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
|
@defclass[wv-settings% object% ()]{
|
||||||
|
|
||||||
|
An OO wrapper around a settings backend implementing the
|
||||||
|
@racket[simple-ini/class] interface.
|
||||||
|
|
||||||
|
A @racket[wv-settings%] object represents a view on one settings section.
|
||||||
|
The supplied @racket[wv-context] value is used as the section identifier for
|
||||||
|
context-local settings.
|
||||||
|
|
||||||
|
The class delegates all actual storage operations to the supplied
|
||||||
|
@racket[ini] object.
|
||||||
|
|
||||||
|
@defconstructor[([ini object?]
|
||||||
|
[wv-context any/c])]{
|
||||||
|
Creates a new settings object.
|
||||||
|
|
||||||
|
The @racket[ini] argument is the backing settings object.
|
||||||
|
|
||||||
|
The @racket[wv-context] argument is used as the section identifier for
|
||||||
|
context-local settings.
|
||||||
|
|
||||||
|
The class assumes that @racket[ini] supports methods compatible with:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket[(send ini get section key)]}
|
||||||
|
@item{@racket[(send ini get section key default)]}
|
||||||
|
@item{@racket[(send ini set! section key value)]}
|
||||||
|
]
|
||||||
|
|
||||||
|
In typical use, @racket[ini] is an instance of a class from
|
||||||
|
@racket[simple-ini/class].
|
||||||
|
}
|
||||||
|
|
||||||
|
@defmethod*[([(get [key any/c]) any/c]
|
||||||
|
[(get [key any/c] [default-value any/c]) any/c])]{
|
||||||
|
Returns the value associated with @racket[key] in the current section.
|
||||||
|
|
||||||
|
Without a default value, this method delegates to:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(send ini get wv-context key)
|
||||||
|
]
|
||||||
|
|
||||||
|
With a default value, it delegates to:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(send ini get wv-context key default-value)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defmethod*[([(set! [key any/c] [value any/c]) this])]{
|
||||||
|
Stores @racket[value] under @racket[key] in the current section.
|
||||||
|
|
||||||
|
This method delegates to:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(send ini set! wv-context key value)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defmethod*[([(get/global [key any/c]) any/c]
|
||||||
|
[(get/global [key any/c] [default-value any/c]) any/c])]{
|
||||||
|
Returns the value associated with @racket[key] in the global section.
|
||||||
|
|
||||||
|
Without a default value, this method delegates to:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(send ini get 'global key)
|
||||||
|
]
|
||||||
|
|
||||||
|
With a default value, it delegates to:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(send ini get 'global key default-value)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defmethod*[([(set/global! [key any/c] [value any/c]) this])]{
|
||||||
|
Stores @racket[value] under @racket[key] in the global section.
|
||||||
|
|
||||||
|
This method delegates to:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(send ini set! 'global key value)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defmethod*[([(clone [context any/c]) (is-a?/c wv-settings%)])]{
|
||||||
|
Creates a new @racket[wv-settings%] object using the same
|
||||||
|
@racket[ini] backend but another section identifier.
|
||||||
|
|
||||||
|
This method is equivalent to:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(new wv-settings% [ini ini] [wv-context context])
|
||||||
|
]
|
||||||
|
|
||||||
|
It is intended to support cheap creation of per-section settings views.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defmethod*[([(context) any/c])]{
|
||||||
|
Returns the section identifier associated with this settings object.
|
||||||
|
}
|
||||||
|
|
||||||
|
} ; end class
|
||||||
Reference in New Issue
Block a user