Documentation and some other stuff.

This commit is contained in:
2026-03-30 11:03:56 +02:00
parent 8349b65a83
commit 9dd8b895ae
13 changed files with 2619 additions and 1607 deletions

View File

@@ -4,10 +4,14 @@
(require "private/wv-context.rkt") (require "private/wv-context.rkt")
(require "private/wv-window.rkt") (require "private/wv-window.rkt")
(require "private/wv-dialog.rkt") (require "private/wv-dialog.rkt")
(require "private/wv-element.rkt")
(require "private/wv-input.rkt")
(provide (all-from-out "private/wv-context.rkt" (provide (all-from-out "private/wv-context.rkt"
"private/wv-window.rkt" "private/wv-window.rkt"
"private/wv-dialog.rkt" "private/wv-dialog.rkt"
"private/wv-element.rkt"
"private/wv-input.rkt"
) )
webview-set-loglevel webview-set-loglevel
webview-version webview-version

View File

@@ -165,7 +165,10 @@
(error (format (error (format
(string-append "Cannot load ~a.\n" (string-append "Cannot load ~a.\n"
"Make sure you installed Qt6 on your system\n" "Make sure you installed Qt6 on your system\n"
"e.g. on debian 'sudo apt install libqt6webenginewidgets6'\n" "NB. the minimum Qt version that is supported is Qt 6.10.\n"
"This probably means you will need to install it separately from\n"
"the standard distro packages (e.g. libqt6webenginewidgets6 on\n"
"debian based systems).\n"
"\n" "\n"
"Exception:\n\n~a") "Exception:\n\n~a")
ffi-library exp))) ffi-library exp)))
@@ -260,9 +263,7 @@
) )
(define-cstruct _rkt_version_t (define-cstruct _rkt_version_t
([qt-major _int] (
[qt-minor _int]
[qt-patch _int]
[api-major _int] [api-major _int]
[api-minor _int] [api-minor _int]
[api-patch _int] [api-patch _int]
@@ -711,16 +712,12 @@
(define (rkt-webview-version) (define (rkt-webview-version)
(let ((d (rkt_webview_version))) (let ((d (rkt_webview_version)))
(let ((v (union-ref (rkt_data_t-data d) 0))) (let ((v (union-ref (rkt_data_t-data d) 0)))
(let ((qt-major (rkt_version_t-qt-major v)) (let ((api-major (rkt_version_t-api-major v))
(qt-minor (rkt_version_t-qt-minor v))
(qt-patch (rkt_version_t-qt-patch v))
(api-major (rkt_version_t-api-major v))
(api-minor (rkt_version_t-api-minor v)) (api-minor (rkt_version_t-api-minor v))
(api-patch (rkt_version_t-api-patch v)) (api-patch (rkt_version_t-api-patch v))
) )
(rkt_webview_free_data d) (rkt_webview_free_data d)
(list (list 'webview-c-api api-major api-minor api-patch) (list (list 'webview-c-api api-major api-minor api-patch))
(list 'qt qt-major qt-minor qt-patch))
) )
) )
) )

View File

@@ -663,7 +663,7 @@
p))))) p)))))
(define (webview-call-js wv js) (define/contract (webview-call-js wv js)
(-> wv-win? string? (or/c string? list? boolean? hash?)) (-> wv-win? string? (or/c string? list? boolean? hash?))
(let ((result (rkt-webview-call-js (wv-win-handle wv) js))) (let ((result (rkt-webview-call-js (wv-win-handle wv) js)))
;(displayln result) ;(displayln result)

View File

@@ -63,28 +63,28 @@
(define/public (unset-style! styles) (define/public (unset-style! styles)
(webview-unset-style! wv element-id styles)) (webview-unset-style! wv element-id styles))
(define (set-attr! attr-entries) (define/public (set-attr! attr-entries)
(webview-set-attr! wv element-id attr-entries)) (webview-set-attr! wv element-id attr-entries))
(define (attr attr) (define/public (attr attr)
(webview-attr wv element-id attr)) (webview-attr wv element-id attr))
(define (attr/number attr) (define/public (attr/number attr)
(webview-attr/number wv element-id attr)) (webview-attr/number wv element-id attr))
(define (attr/symbol attr) (define/public (attr/symbol attr)
(webview-attr/symbol wv element-id attr)) (webview-attr/symbol wv element-id attr))
(define (attr/boolean attr) (define/public (attr/boolean attr)
(webview-attr/boolean wv element-id attr)) (webview-attr/boolean wv element-id attr))
(define (attr/date attr) (define/public (attr/date attr)
(webview-attr/date wv element-id attr)) (webview-attr/date wv element-id attr))
(define (attr/time attr) (define/public (attr/time attr)
(webview-attr/time wv element-id attr)) (webview-attr/time wv element-id attr))
(define (attr/datetime attr) (define/public (attr/datetime attr)
(webview-attr/datetime wv element-id attr)) (webview-attr/datetime wv element-id attr))
(super-new) (super-new)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,516 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1400"
height="820"
viewBox="0 0 1400 820"
version="1.1"
id="svg34"
sodipodi:docname="rktwebview-shared-memory-diagram-simple.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs34">
<marker
style="overflow:visible"
id="marker49"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path49" />
</marker>
<marker
style="overflow:visible"
id="marker48"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path48" />
</marker>
<marker
style="overflow:visible"
id="marker45"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path45" />
</marker>
<marker
style="overflow:visible"
id="Triangle"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path135" />
</marker>
<marker
style="overflow:visible"
id="marker40"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path40" />
</marker>
<marker
style="overflow:visible"
id="marker38"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path38" />
</marker>
</defs>
<sodipodi:namedview
id="namedview34"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.78374771"
inkscape:cx="729.82669"
inkscape:cy="468.90089"
inkscape:window-width="1920"
inkscape:window-height="1129"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg34" />
<rect
x="0"
y="0"
width="1400"
height="820"
fill="#ffffff"
id="rect1" />
<!-- Title -->
<rect
x="292.14935"
y="50.869148"
width="807.46985"
height="69.175186"
fill="#4b7fda"
stroke="#000000"
stroke-width="2.82481"
id="rect2"
ry="14.206594"
rx="26.788099" />
<text
x="693.3324"
y="95.456741"
font-family="Arial"
font-size="30px"
font-weight="bold"
text-anchor="middle"
fill="#ffffff"
id="text2">Shared Memory</text>
<!-- Shared memory block -->
<rect
x="298.4361"
y="138.76608"
width="800"
height="600"
fill="#efefef"
stroke="#000000"
stroke-width="2"
id="rect3" />
<!-- Allocator -->
<g
id="g36"
transform="translate(-1.5639013,-1.2339131)">
<g
id="g34"
transform="translate(-24.328594,-25.434439)">
<rect
x="350"
y="185"
width="240"
height="60"
fill="#d8d8d8"
stroke="#000000"
stroke-width="2"
id="rect4"
ry="11.058452"
rx="12.164297" />
</g>
<text
x="445.67142"
y="197.56557"
font-family="Arial"
font-size="24px"
font-weight="bold"
text-anchor="middle"
id="text4">Allocator</text>
</g>
<text
x="311.94321"
y="237.80243"
font-family="Arial"
font-size="16px"
id="text5">free list, slot table, allocation headers</text>
<!-- Queues -->
<rect
x="633.4361"
y="183.76608"
width="395"
height="54"
fill="#f0bc23"
stroke="#000000"
stroke-width="2"
id="rect5" />
<rect
x="633.4361"
y="237.76608"
width="395"
height="50"
fill="#fbf6df"
stroke="#000000"
stroke-width="2"
id="rect6" />
<text
x="830.4361"
y="218.76608"
font-family="Arial"
font-size="24px"
font-weight="bold"
text-anchor="middle"
id="text6">Command Queue</text>
<text
x="830.4361"
y="269.76608"
font-family="Arial"
font-size="18px"
text-anchor="middle"
id="text7">command payloads</text>
<rect
x="633.4361"
y="348.76608"
width="395"
height="54"
fill="#71b53b"
stroke="#000000"
stroke-width="2"
id="rect7" />
<rect
x="633.4361"
y="402.76608"
width="395"
height="50"
fill="#edf5e0"
stroke="#000000"
stroke-width="2"
id="rect8" />
<text
x="830.4361"
y="383.76608"
font-family="Arial"
font-size="24px"
font-weight="bold"
text-anchor="middle"
fill="#ffffff"
id="text8">Result Queue</text>
<text
x="830.4361"
y="434.76608"
font-family="Arial"
font-size="18px"
text-anchor="middle"
id="text9">result payloads</text>
<rect
x="633.4361"
y="513.76611"
width="395"
height="54"
fill="#e56a0b"
stroke="#000000"
stroke-width="2"
id="rect9" />
<rect
x="633.4361"
y="567.76611"
width="395"
height="50"
fill="#fae9dd"
stroke="#000000"
stroke-width="2"
id="rect10" />
<text
x="830.4361"
y="548.76611"
font-family="Arial"
font-size="24px"
font-weight="bold"
text-anchor="middle"
fill="#ffffff"
id="text10">Event Queue</text>
<text
x="830.4361"
y="599.76611"
font-family="Arial"
font-size="18px"
text-anchor="middle"
id="text11">event payloads</text>
<!-- Payload label -->
<text
x="698.4361"
y="673.76611"
font-family="Arial"
font-size="24px"
font-weight="bold"
text-anchor="middle"
fill="#555555"
id="text12">Payloads in Shared Memory</text>
<line
x1="388.4361"
y1="686.76611"
x2="1008.4361"
y2="686.76611"
stroke="#888888"
stroke-width="2"
id="line12" />
<!-- Left process -->
<rect
x="43.653984"
y="361.71698"
width="274.56424"
height="326.83121"
fill="#ffffff"
stroke="#000000"
stroke-width="2"
id="rect12" />
<g
id="g37"
transform="translate(-1.7755917,-110.71258)">
<rect
x="45"
y="470"
width="275"
height="90"
fill="#e26a10"
stroke="#000000"
stroke-width="2"
id="rect13" />
<text
x="182"
y="525"
font-family="Arial"
font-size="28px"
font-weight="bold"
text-anchor="middle"
fill="#ffffff"
id="text13">Racket Process</text>
</g>
<text
x="101.61144"
y="514.19263"
font-family="Arial"
font-size="22px"
font-weight="bold"
id="text14">• Call API (FFI)</text>
<text
x="117.09328"
y="543.13422"
font-family="Arial"
font-size="22px"
font-weight="bold"
id="text15">• Receive Result</text>
<text
x="96.082222"
y="589.76929"
font-family="Arial"
font-size="22px"
font-weight="bold"
id="text16">• Poll Events</text>
<!-- Right process -->
<g
id="g44"
transform="translate(15.023069,-161.99993)">
<rect
x="1120"
y="460"
width="245"
height="240"
fill="#f7f7f8"
stroke="#000000"
stroke-width="2"
id="rect16" />
<rect
x="1120"
y="460"
width="245"
height="105"
fill="#676e7a"
stroke="#000000"
stroke-width="2"
id="rect17" />
<text
x="1242"
y="515"
font-family="Arial"
font-size="24px"
font-weight="bold"
text-anchor="middle"
fill="#ffffff"
id="text17">rktwebview_prg</text>
<text
x="1242"
y="548"
font-family="Arial"
font-size="18px"
text-anchor="middle"
fill="#ffffff"
id="text18">(Qt / Chromium)</text>
<g
id="g41"
transform="translate(0,-11.058452)">
<rect
x="1152"
y="590"
width="180"
height="48"
fill="#efdf9b"
stroke="#000000"
stroke-width="2"
id="rect18" />
<text
x="1242"
y="621"
font-family="Arial"
font-size="20px"
font-weight="bold"
text-anchor="middle"
id="text19">Command Thread</text>
</g>
<g
id="g42">
<g
id="g43"
transform="translate(0,-10.947309)">
<rect
x="1152"
y="650"
width="180"
height="48"
fill="#efdf9b"
stroke="#000000"
stroke-width="2"
id="rect19" />
<text
x="1242"
y="681"
font-family="Arial"
font-size="20px"
font-weight="bold"
text-anchor="middle"
id="text20">Qt GUI Thread</text>
</g>
</g>
</g>
<!-- Command arrows -->
<!-- Result arrows right to left -->
<!-- Event arrows right to left -->
<!-- Footer -->
<text
x="698.4361"
y="783.76611"
font-family="Arial"
font-size="16px"
text-anchor="middle"
fill="#555555"
id="text34">Racket calls the C API through FFI; the helper process consumes commands and produces results and events.</text>
<path
style="fill:#000000;stroke:#000000;stroke-width:4.2;stroke-dasharray:none;marker-end:url(#marker38)"
d="m 638.72046,571.59389 -321.80095,1.10585"
id="path37"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:4.1534;stroke-dasharray:none;marker-end:url(#marker40)"
d="m 635.3126,404.52149 -55.08407,1.10725 -2.16016,128.43983 -260.29922,2.21448"
id="path39" />
<path
style="fill:none;stroke:#000000;stroke-width:4.2;stroke-dasharray:none;marker-end:url(#Triangle)"
d="M 306.96691,505.24318 H 543.61777 V 261.95724 h 87.36177"
id="path41" />
<path
style="fill:none;stroke:#000000;stroke-width:3.89804;stroke-dasharray:none;marker-end:url(#marker45)"
d="m 1165.3148,499.58917 h -86.7061 v 77.63074 h -36.8501"
id="path44" />
<path
style="fill:none;stroke:#000000;stroke-width:3.9801;stroke-dasharray:none;marker-end:url(#marker49)"
d="m 1030.6475,241.07822 h 31.4718 v 189.62297 h 95.6253"
id="path46" />
<path
style="fill:none;stroke:#000000;stroke-width:4.2;stroke-dasharray:none;marker-end:url(#marker48)"
d="m 1162.0759,452.99389 h -112.2811 v -53.58867 l -18.8137,5.04111"
id="path47" />
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,346 +1,256 @@
#lang scribble/manual #lang scribble/manual
@title{Structure and behavior of @tt{rktwebview_qt}} @title{Qt WebView Backend Architecture}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@section{Introduction}
It would, of course, be preferable to place everything within a single process,
under a unified structure. This would be elegant. It is not how things are.
@centered{
@image[#:scale 0.45]{rktwebview-shared-memory-diagram-simple.svg}
}
Qt WebEngine establishes its own order: threads, event loops, internal state.
Once set in motion, it does not easily yield. It persists, and it expects its
environment to adapt accordingly. These conditions are accepted.
The Racket process, however, is of a different nature. It is light, precise,
capable of starting and stopping without residue. It must remain so.
So a boundary is drawn.
On one side, Qt: a large, immovable instrument—something like an organ. Once it
begins to sound, it fills the space, and it is not easily silenced. On the other,
Racket: a violin, agile and expressive, able to begin and end a phrase at will.
They do not become the same instrument. They are allowed to play together.
Communication is arranged accordingly. A shared memory region, containing three
queues: commands, results, and events. A command is issued. It crosses the boundary. It is taken up and executed. A result returns.
Events also arise, independently, and must be handled when they appear.
Within this structure, the violin may move freely—provided it does not attempt to
reconfigure the organ. No attempt is made to unify the instruments. Such efforts would not improve the music. Instead, the composition is written so that each plays its part.
From the outside, one hears only a simple exchange: a call, a response. Internally, the balance is carefully maintained. For now, this is sufficient. And it holds.
@section{Overview} @section{Overview}
@tt{rktwebview_qt} is a Qt-based backend that provides embedded webviews for This backend provides a webview implementation by delegating all GUI and browser
applications written in Racket. The library exposes a small C-compatible API functionality to a separate Qt process.
intended to be used through the Racket FFI. Internally, the implementation is
written in C++ and uses Qt WebEngine.
The backend provides: The embedding Racket process does not manipulate Qt widgets directly. Instead,
it communicates with a helper process that owns the Qt event loop and all
@tt{QWebEngine} objects.
@itemlist[#:style 'compact This design exists to work around limitations of Qt WebEngine in combination with
@item{creation of browser windows} the lifecycle model of the DrRacket environment.
@item{navigation to URLs or HTML content}
@item{execution of JavaScript}
@item{return values from JavaScript}
@item{delivery of browser and window events}
@item{native dialogs such as file choosers and message boxes}
]
The implementation is intentionally organized so that the external interface @section{Execution Model}
remains simple and language-neutral while the Qt-specific logic is hidden
inside the runtime controller.
@section{Requirements} The runtime consists of two processes: the embedding Racket process and a helper
process running Qt and Qt WebEngine.
The @tt{rktwebview_qt} backend requires Qt version @tt{6.10.2} or newer. All GUI state lives in the helper process. The embedding side holds no direct
references to Qt objects. Communication is explicit and happens through shared
memory.
The backend is built against Qt WebEngine and depends on the runtime @section{Shared Memory and Queues}
components provided by Qt. Earlier Qt versions are not supported.
Applications using the backend must therefore ensure that a compatible A shared memory region is created during initialization. Inside that region,
Qt runtime (≥ @tt{6.10.2}) is available. three FIFO queues are established: a command queue, a result queue, and an event
queue.
In packaged builds the required Qt runtime is normally bundled together Each message consists of a numeric code and a payload, typically JSON:
with the backend library.
@subsection{Compatibility} @centerline{@tt{(code, payload)}}
The backend requires Qt version @tt{6.10.2} or newer. The queues have distinct roles. The @italic{command queue} carries requests from the embedding process to the Qt process, for example creating a window, loading a URL, or executing JavaScript. The @italic{result queue} carries direct replies to those commands. A synchronous call on the embedding side blocks until a corresponding result is available. The @italic{event queue} carries asynchronous notifications generated by the Qt side, such as page load completion, navigation requests, window movement, or events
originating from JavaScript.
This requirement is due to the use of the method @section{Command Execution}
@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 A function call on the embedding side is translated into a command and written to
local services. Earlier Qt versions do not provide this functionality the command queue.
and are therefore not supported.
Packaged builds typically include the required Qt runtime components. From there the flow is fixed: (1) the command is read by a worker thread in the
helper process, (2) it is reposted onto the Qt GUI thread, (3) the GUI thread
executes the operation, and (4) the result is written back to the result queue.
@section{Layers} The worker thread never manipulates Qt objects. All GUI work happens on the GUI
thread.
The system consists of several layers. From the callers perspective, a synchronous call returns only after the GUI
thread has completed the action.
@subsection{C ABI Layer} @section{Event Delivery}
The file @tt{rktwebview.h} defines the public API. This layer provides a stable Many relevant events are not tied to a specific command. Page loading, navigation
C-compatible interface that can easily be consumed from Racket through FFI. attempts, window movement, and JavaScript-originated events are delivered through
the event queue.
Important characteristics of the ABI include: Events are retrieved explicitly by polling.
@itemlist[#:style 'compact Each event contains a name, an identifier, and optional fields depending on its
@item{numeric handles represent windows and browser contexts} type. Events are delivered in FIFO order.
@item{result codes indicate success or failure}
@item{data returned from the library is represented as @tt{rkt_data_t}}
@item{event callbacks deliver JSON strings describing events}
]
The C layer is implemented in @tt{rktwebview.cpp}. These functions mainly @section{Contexts and Webviews}
forward calls to a singleton instance of the internal runtime class
@tt{Rktwebview_qt}.
@section{Runtime Controller} The backend uses a two-level model consisting of contexts and webviews.
The class @tt{Rktwebview_qt} acts as the central runtime controller. A context represents a browser environment and corresponds to a
@tt{QWebEngineProfile}. It defines how pages run, including injected scripts and optional trust configuration using explicitly trusted self-signed certificates.
Its responsibilities include: Each context is identified by an integer handle.
@itemlist[#:style 'compact Within a context, one or more webviews can be created. A webview represents a
@item{owning the Qt application instance} window containing a browser view. Webviews are also identified by integer
@item{managing https contexts} handles.
@item{managing open windows}
@item{dispatching commands}
@item{forwarding events to the host application}
]
Internally, it maintains several registries: A webview always belongs to exactly one context. When creating a webview, the
context handle must be provided.
@itemlist[#:style 'compact Webviews may optionally have a parent webview. If a parent is specified, the
@item{a map from window handles to @tt{WebviewWindow} instances} resulting window is created as a modal child of that parent; otherwise it is
@item{a map from context identifiers to @tt{QWebEngineProfile} objects} created as a top-level window.
@item{a map from window handles to callback functions}
]
Each API call that manipulates a window or browser state is forwarded through From the Racket side, this means that a context must be created first. That
this controller. context handle is then used to create webviews, which are subsequently addressed
through their own handles.
@section{HTTP(s) Contexts} All Qt objects remain internal to the helper process; only these integer handles
cross the process boundary.
A http(s) context represents a shared WebEngine profile. @section{JavaScript Bridge}
Contexts are created using @tt{rkt_webview_new_context}. Each context contains: Each context installs a small JavaScript bridge into every page, allowing
JavaScript code to send structured data to the host via:
@itemlist[#:style 'compact @centerline{@tt{window.rkt_send_event(obj)}}
@item{a @tt{QWebEngineProfile}}
@item{optional injected JavaScript code}
@item{optional trusted certificates}
]
Multiple windows normally share the same context. This allows cookies, The objects are collected and forwarded to the event queue.
configuration, and injected scripts to be reused.
A context normally represents a https server that can be reached on a certain (local) Url. @section{Navigation and Window Behavior}
@section{Window and View Classes} User actions are not always executed immediately; navigation initiated by the
user may result in a @tt{"navigation-request"} event instead of being followed
automatically, and closing a window may result in a @tt{"can-close?"} event. The
Racket side is expected to decide how to handle these situations.
Two Qt classes implement the actual browser window. @section{Design Considerations}
@subsection{@tt{WebviewWindow}} @bold{Qt WebEngine lifecycle.}
Qt WebEngine cannot be safely reinitialized within a single process.
@tt{WebviewWindow} derives from @tt{QMainWindow}. It represents the native In practice, once a @tt{QApplication} using WebEngine has been started and shut
top-level window. down, the WebEngine runtime cannot be started again. This is a known and
documented limitation (see for example QTBUG-70519, QTBUG-87460,
QTBUG-145033). The underlying cause is that WebEngine starts internal threads
and resources that are not fully released, even after application shutdown.
Responsibilities include: Attempts to reinitialize WebEngine in the same process result in undefined
behavior, including crashes, hangs, or inconsistent state.
@itemlist[#:style 'compact In the DrRacket environment, where components may be restarted under a
@item{hosting the browser widget} custodian, this makes an in-process design fundamentally unsuitable. Even when
@item{reporting window events such as show, hide, move, and resize} libraries are loaded and unloaded using @tt{#:custodian}, the WebEngine runtime
@item{handling close requests} cannot be reset to a clean state.
@item{reporting navigation events}
@item{forwarding JavaScript events}
]
Window geometry changes are slightly delayed through timers so that the host By moving Qt and WebEngine into a separate process, this limitation is avoided
application receives fewer intermediate events. entirely: each start of the backend creates a fresh runtime, and terminating the
helper process guarantees that all associated threads and resources are released
by the operating system.
@subsection{@tt{WebViewQt}} @bold{Event loop and threading.}
Qt requires that GUI operations are performed on the Qt GUI thread.
@tt{WebViewQt} derives from @tt{QWebEngineView}. This class mainly associates Instead of attempting to integrate Qts event loop with Racket, the design
the Qt widget with a numeric handle used by the external API. isolates Qt completely and runs it in its own process.
@section{Command Dispatch} Within that process, a worker thread receives commands and forwards them to the
GUI thread using Qts event mechanism (via @tt{postEvent}). The Racket side never
interacts with Qt objects directly.
Many API functions are implemented using a small command-dispatch mechanism. @bold{Failure isolation.}
Qt WebEngine is a large subsystem with its own internal processes (including the
Chromium-based @tt{QtWebEngineProcess}) and is generally stable in practice.
A command object represents a pending request. The request is delivered to the Running the Qt side in a separate process provides isolation: if the helper
Qt event system using a custom Qt event. The runtime controller receives the process terminates, the embedding Racket process remains unaffected and can
event and executes the corresponding operation. decide how to recover.
This design ensures that GUI operations occur inside the Qt event loop while @bold{Shared memory communication.}
the external API remains synchronous. The communication pattern consists of commands, results, and events, mapped onto
shared memory FIFO queues, keeping the model simple and explicit.
@section{Event Processing} @bold{JSON encoding.}
All payloads are encoded as JSON, providing a natural bridge between JavaScript,
Qt/C++, and Racket: JavaScript produces JSON natively, Qt maps it to variant
types, and Racket can decode it easily.
Qt events are processed through repeated calls to For control commands, payload sizes are small and infrequent, so serialization
@tt{rkt_webview_process_events}, which internally calls cost is negligible compared to GUI-thread execution and WebEngine processing; for
@tt{QApplication::processEvents}. This allows the host runtime to control how dynamic data such as JavaScript results and custom events, JSON is the
frequently GUI events are processed. appropriate representation. A binary protocol would reduce overhead but increase
complexity and reduce inspectability.
@section{Navigation} @section{Shared Memory Architecture}
Two main navigation operations are provided. Communication between the Racket process and @tt{rktwebview_prg} is implemented
using a shared memory region. This region serves three purposes at once: it
stores shared data structures, it provides a simple allocator, and it hosts the
FIFO queues used for message passing.
@itemlist[#:style 'compact At the start of the shared memory block, a small administration area is stored,
@item{@tt{rkt_webview_set_url} loads a URL} including a pointer to the current end of allocated memory, a list of active
@item{@tt{rkt_webview_set_html} loads raw HTML} allocations, a free list, and a fixed slot table. The slot table acts as a
] directory of shared objects; queues are created once, stored in slots, and can be
retrieved by both processes using only the slot number.
When page loading finishes, a @tt{"page-loaded"} event is emitted. The event Memory allocation inside the shared block is intentionally simple. Each
contains a boolean field indicating whether loading succeeded. allocation is preceded by a small header containing size information and links
for a double-linked list. Allocation first attempts to reuse a block from the
@section{JavaScript Execution} free list; if no suitable block is available, memory is taken from the unused
tail of the region. Freed blocks are returned to the free list and may later be
JavaScript execution is provided through two functions. reused. Blocks are not compacted or coalesced. This is not a general-purpose
heap; it is a small, predictable allocator for queue items and payload strings.
@subsection{Fire-and-Forget Execution}
Shared objects are referenced primarily through offsets (via @tt{ShmPlace})
@tt{rkt_webview_run_js} executes JavaScript without returning a result. rather than raw pointers. This makes the layout independent of the virtual
address at which the shared memory is mapped in each process.
This is useful for DOM manipulation or triggering page behavior.
Queues are built directly on top of this allocator. Each queue consists of a
@subsection{Synchronous Execution with Result} small header containing the first item, the last item, and a count, followed by a
linked list of queue items. Each item stores a numeric command or event code, a
@tt{rkt_webview_call_js} evaluates JavaScript and returns a result object. pointer to its payload in shared memory, and links to neighboring items.
The JavaScript is wrapped in a small function that captures either the result Synchronization is split into two layers. A shared lock protects allocator and
value or an exception. The result is returned as a JSON object containing: queue metadata, while each queue has its own semaphore indicating whether items
are available. One mechanism protects the structure; the other tells you whether
@itemlist[#:style 'compact there is anything worth reading.
@item{@tt{oke}}
@item{@tt{result}} On POSIX systems such as Linux, shared memory is implemented using
@item{@tt{exn}} @tt{shm_open}, @tt{ftruncate}, and @tt{mmap}, with synchronization via named
] POSIX semaphores created using @tt{sem_open}. The owner process initializes these
objects and removes them again using @tt{shm_unlink} and @tt{sem_unlink}.
The native side waits for the asynchronous Qt callback before returning the
value to the host application. On Windows, the same model is implemented using @tt{CreateFileMappingA} and
@tt{MapViewOfFile} for shared memory, and @tt{CreateSemaphoreA} or
@section{JavaScript Event Bridge} @tt{OpenSemaphoreA} for synchronization. The design is identical, but the kernel
objects follow the Windows lifetime model and are released when the last handle
Communication from JavaScript to the host application uses a small event queue is closed.
implemented in injected JavaScript.
The shared memory region has a fixed size (currently 10MB) and is not resized at
When a browser context is created, the runtime injects support code defining runtime. Although the use of @tt{ShmPlace} offsets would in principle allow
the following objects: relocation, resizing would require coordinated remapping in both processes while
all activity is paused. The current design therefore treats the region as
@itemlist[#:style 'compact fixed-size and relies on reuse of freed blocks.
@item{@tt{window.rkt_event_queue}}
@item{@tt{window.rkt_send_event(obj)}} This implies that the communication channel is bounded. Payloads such as
@item{@tt{window.rkt_get_events()}} @tt{set_html} or large JavaScript results must fit within the available free
] space in the shared memory block. In practice, the usable limit is somewhat below
the nominal 10MB due to allocator overhead, queue administration, and concurrent
The queue stores JavaScript-generated events until the native side retrieves messages.
them.
This is a message transport, not an infinite sack of HTML.
@subsection{Event Queue}
When page code wants to emit an event, it calls:
@verbatim|{
window.rkt_send_event(obj)
}|
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
@item{JavaScript calls @tt{rkt_send_event}.}
@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.}
]
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.
@subsection{Delivered Event Format}
JavaScript-originated events are forwarded to the host as structured JSON.
Each event uses a generic envelope:
@verbatim|{
{
"evt": "js-evt",
"js-evt": { ... }
}
}|
The inner object is the original JavaScript event payload.
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
@item{@tt{"show"}}
@item{@tt{"hide"}}
@item{@tt{"move"}}
@item{@tt{"resize"}}
@item{@tt{"closed"}}
]
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.
@section{Native Dialogs}
The backend also exposes native dialogs including:
@itemlist[#:style 'compact
@item{directory selection}
@item{file open}
@item{file save}
@item{message boxes}
]
The results of these dialogs are delivered as events so that the host
application remains in control of the user interface flow.
@section{Certificates}
Contexts may optionally install trusted certificates. Additionally, a window
may specify a special Organizational Unit token used to automatically trust
certain self-signed certificates.
This mechanism is useful when communicating with local development services.
@section{Memory Ownership}
Data returned from the library and (javascript) events originating from the library
are allocated on the native side and must be released by the host using
@tt{rkt_webview_free_data}.
This avoids exposing C++ ownership semantics across the FFI boundary.
@section{Summary}
The @tt{rktwebview_qt} backend provides a compact architecture for embedding
Qt WebEngine inside Racket applications.
The design allows Racket programs to implement desktop applications using
HTML and JavaScript for the user interface while keeping application logic in
the host runtime.

View File

@@ -1,80 +1,107 @@
#lang scribble/manual #lang scribble/manual
@(require scribble/racket @(require racket/base
scribble/example racket/class
scribble/core scribble/core
(for-label racket/base (for-label racket/base
racket/string
racket/class racket/class
racket/path racket/file))
"../private/wv-settings.rkt"
"../private/racket-webview.rkt")
)
@defmodule[racket-webview] @; "../private/wv-context.rkt")
@title{Object-Oriented Interface: @racket[wv-context%]} @title{wv-context}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[wv-context]
@section{Overview}
The library is organized around two main concepts: contexts and windows.
A context represents the shared runtime environment for one or more webview
windows. It owns the underlying webview context, provides the base URL used by
those windows, and gives access to persistent settings through
@racketmodname[wv-settings].
This module exports the @racket[wv-context%] class.
@defclass[wv-context% object% ()]{ @defclass[wv-context% object% ()]{
An OO wrapper around a webview context together with its associated settings. Represents a webview context.
A @racket[wv-context%] object creates and owns a webview context by calling
@racket[webview-new-context]. It also creates a corresponding @racket[wv-settings%] A @racket[wv-context%] object is a thin class-based wrapper around the
instance for access to global and per-section settings. This class manages higher-level context support provided by @racketmodname[racket-webview]. It
construction of a webview context using a file getter and boilerplate JavaScript, creates and stores a @racket[wv-context] value internally and provides methods
exposure of the created low-level context object via @racket[context], to access that value, its base URL, and a settings object.
access to the base URL of the created context via @racket[base-url] and
access to settings views via @racket[settings].
@defconstructor[([base-path path-string?] @defconstructor[([base-path path-string?]
[file-getter procedure? (webview-standard-file-getter base-path)] [file-getter procedure?
[context-js procedure? (λ () "")] (webview-standard-file-getter base-path)]
[boilerplate-js procedure? (webview-default-boilerplate-js context-js)] [context-js procedure?
[ini object?])]{ (λ () "")]
Creates a new @racket[wv-context%] object. [boilerplate-js string?
(webview-default-boilerplate-js context-js)]
[ini any/c
(error "You need to provide a 'ini' file settings interface for settings, e.g. simple-ini/class")])]{
The @racket[base-path] argument is used to construct the default Creates a new context object.
@racket[file-getter].
The @racket[file-getter] argument is passed to @racket[webview-new-context] The constructor accepts the following initialization fields.
and is responsible for resolving files served by the context.
The @racket[context-js] argument must be a procedure producing the JavaScript
snippet that should be injected into the context.
The @racket[boilerplate-js] argument must be a procedure producing the final
boilerplate JavaScript used when constructing the context. By default it is
built from @racket[context-js] using @racket[webview-default-boilerplate-js].
The @racket[ini] argument is mandatory and supplies the settings backend used
for construction of the internal @racket[wv-settings%] object. If omitted, the
constructor raises an error.
During initialization the class:
@itemlist[#:style 'compact @itemlist[#:style 'compact
@item{creates a new low-level context with @racket[webview-new-context];} @item{@racket[base-path] is the base path used by the default file getter.}
@item{creates a @racket[wv-settings%] object using @racket[ini] and the @item{@racket[file-getter] is the procedure used to resolve files for the
global context identifier @racket['global].} local web server. Its default value is
@racket[(webview-standard-file-getter base-path)].}
@item{@racket[context-js] is a procedure producing additional JavaScript for
the context. Its default value is @racket[(λ () "")].}
@item{@racket[boilerplate-js] is the JavaScript boilerplate installed into the
underlying webview context. Its default value is
@racket[(webview-default-boilerplate-js context-js)].}
@item{@racket[ini] is the settings backend used to construct the associated
@racket[wv-settings%] object. No default backend is provided; omitting it
raises an error.}]
After @racket[super-new], the constructor creates the underlying
@racket[wv-context] value using @racket[webview-new-context], and then creates a
settings object using:
@racketblock[
(new wv-settings% [ini ini] [wv-context 'global])
] ]
The settings object is stored internally and used by the
@racket[settings] method.
} }
@defmethod*[([(context) wv-context?])]{ @defmethod[(context) wv-context?]{
Returns the underlying webview context (struct) created during
initialization. Returns the internal context value created by @racket[webview-new-context].
This is the underlying @racketmodname[racket-webview] context object used by
the rest of the library.
} }
@defmethod*[([(settings [section symbol?]) (is-a?/c wv-settings%)])] { @defmethod[(settings [section symbol?]) wv-settings%]{
Returns a @tt{wv-settings%} object for @racket[section].
This method delegates to the internally stored @racket[wv-settings%] object by Returns a cloned settings object for @racket[section].
calling @racket[(send settings-obj clone section)]. The resulting object shares
the same @racket[ini] backend as the global settings object, but addresses the The method delegates to the internal @racket[wv-settings%] instance as:
supplied section.
@racketblock[
(send settings-obj clone section)
]
This allows per-section settings access while preserving the original settings
object stored by the context.
} }
@defmethod*[([(base-url) any/c])] { @defmethod[(base-url) string?]{
Returns the base URL of the underlying webview context.
This method delegates to @racket[wv-context-base-url].
}
Returns the base URL of the underlying context.
The method delegates to @racket[wv-context-base-url] applied to the internal
context value.
}
} }

283
scrbl/wv-element.scrbl Normal file
View File

@@ -0,0 +1,283 @@
#lang scribble/manual
@(require racket/base
racket/class
scribble/core
(for-label racket/base
racket/string
racket/class
"../private/wv-window.rkt"
"../private/racket-webview.rkt"))
@title{wv-element}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[wv-element]
DOM element wrapper used by the window layer.
This module exports the @racket[wv-element%] class. Instances of this class
represent one DOM element within a @racket[wv-window%] and provide a small
object-oriented interface for event dispatch, content replacement, CSS class
manipulation, style access, and attribute access.
@section{Overview}
A @racket[wv-element%] object is associated with:
@itemlist[#:style 'compact
@item{one window}
@item{one DOM element identifier}]
The class delegates DOM operations to the JavaScript-based API from
@racketmodname[racket-webview]. The accepted argument shapes and result values
of its methods therefore follow the contracts of those lower-level functions.
The class also stores per-element event callbacks used by @racket[wv-window%]
when dispatching JavaScript events received from the browser.
@section{Class: wv-element%}
@defclass[wv-element% object% ()]{
Represents one DOM element identified by its id.
The class stores the owning window and the element id supplied at construction
time. It also maintains an internal hash table mapping event symbols to
callbacks.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates an element wrapper.
@racket[window] is the owning window object. @racket[element-id] is the DOM
element identifier used in calls to the lower-level DOM manipulation functions.
}
@defmethod[(id) symbol?]{
Returns the element id.
}
@defmethod[(add-event-callback! [evt symbol?] [cb procedure?]) void?]{
Associates @racket[cb] with @racket[evt].
If a callback was already stored for @racket[evt], it is replaced.
}
@defmethod[(remove-event-callback! [evt symbol?]) void?]{
Removes the callback associated with @racket[evt], if any.
}
@defmethod[(event-callback-count) exact-integer?]{
Returns the number of registered event callbacks.
}
@defmethod[(dispatch-event [evt symbol?] [data any/c])
(or/c 'wv-unhandled-js-event 'wv-handled-js-event)]{
Dispatches an event to the callback registered for @racket[evt].
If no callback is registered, the method returns
@racket['wv-unhandled-js-event].
If a callback is found, it is invoked as:
@racketblock[
(cb this evt data)
]
and the method returns @racket['wv-handled-js-event].
}
@defmethod[(set-innerHTML! [html (or/c string? xexpr?)])
(is-a?/c wv-element%)]{
Sets the @tt{innerHTML} of this element using JavaScript and returns this
element.
The operation delegates to:
@racketblock[
(webview-set-innerHTML! wv element-id html)
]
}
@defmethod[(add-class! [cl (or/c symbol? string? list?)]) any/c]{
Adds one or more CSS classes to this element.
The accepted values follow the contract of @racket[webview-add-class!]. The
operation delegates to:
@racketblock[
(webview-add-class! wv element-id cl)
]
}
@defmethod[(remove-class! [cl (or/c symbol? string? list?)]) any/c]{
Removes one or more CSS classes from this element.
The accepted values follow the contract of @racket[webview-remove-class!]. The
operation delegates to:
@racketblock[
(webview-remove-class! wv element-id cl)
]
}
@defmethod[(display [d (or/c symbol? string?)] ...) any/c]{
Gets or sets the CSS @tt{display} property of this element.
If no argument is supplied, the current value is returned by delegating to:
@racketblock[
(webview-get-style wv element-id 'display)
]
If an argument is supplied, only the first argument is used. The value may be a
symbol or string and is converted internally before being passed to
@racket[webview-set-style!].
The resulting style value is then read back and returned.
}
@defmethod[(visibility [v (or/c symbol? string?)] ...) any/c]{
Gets or sets the CSS @tt{visibility} property of this element.
If no argument is supplied, the current value is returned by delegating to:
@racketblock[
(webview-get-style wv element-id 'visibility)
]
If an argument is supplied, only the first argument is used. The value may be a
symbol or string and is converted internally before being passed to
@racket[webview-set-style!].
The resulting style value is then read back and returned.
}
@defmethod[(set-style! [styles (or/c kv? list-of-kv?)]) any/c]{
Sets one or more inline style properties on this element.
The accepted values follow the contract of @racket[webview-set-style!]. The
method delegates to:
@racketblock[
(webview-set-style! wv element-id styles)
]
}
@defmethod[(unset-style! [styles (or/c symbol? list-of-symbol?)]) any/c]{
Clears one or more inline style properties on this element.
The accepted values follow the contract of @racket[webview-unset-style!]. The
method delegates to:
@racketblock[
(webview-unset-style! wv element-id styles)
]
}
@defmethod[(set-attr! [attr-entries (or/c kv? list-of-kv?)]) any/c]{
Sets one or more attributes on this element.
The accepted values follow the contract of @racket[webview-set-attr!]. The
method delegates to:
@racketblock[
(webview-set-attr! wv element-id attr-entries)
]
}
@defmethod[(attr [attr (or/c symbol? string?)]) (or/c string? boolean?)]{
Returns the value of the attribute identified by @racket[attr].
The result follows the contract of @racket[webview-attr]. If the underlying
JavaScript result is @tt{null}, the lower layer returns @racket[#f].
The method delegates to:
@racketblock[
(webview-attr wv element-id attr)
]
}
@defmethod[(attr/number [attr (or/c symbol? string?)]) (or/c number? #f)]{
Returns the attribute value converted to a number.
The method delegates to:
@racketblock[
(webview-attr/number wv element-id attr)
]
}
@defmethod[(attr/symbol [attr (or/c symbol? string?)]) (or/c symbol? #f)]{
Returns the attribute value converted to a symbol.
The method delegates to:
@racketblock[
(webview-attr/symbol wv element-id attr)
]
}
@defmethod[(attr/boolean [attr (or/c symbol? string?)]) (or/c boolean? #f)]{
Returns the attribute value converted to a boolean.
The method delegates to:
@racketblock[
(webview-attr/boolean wv element-id attr)
]
}
@defmethod[(attr/date [attr (or/c symbol? string?)]) any/c]{
Returns the attribute value converted to a date.
The method delegates to:
@racketblock[
(webview-attr/date wv element-id attr)
]
}
@defmethod[(attr/time [attr (or/c symbol? string?)]) any/c]{
Returns the attribute value converted to a time.
The method delegates to:
@racketblock[
(webview-attr/time wv element-id attr)
]
}
@defmethod[(attr/datetime [attr (or/c symbol? string?)]) any/c]{
Returns the attribute value converted to a datetime.
The method delegates to:
@racketblock[
(webview-attr/datetime wv element-id attr)
]
}
}

386
scrbl/wv-input.scrbl Normal file
View File

@@ -0,0 +1,386 @@
#lang scribble/manual
@(require racket/base
racket/class
scribble/core
(for-label racket/base
racket/class
"../private/racket-webview.rkt"
"../private/wv-element.rkt"
"../private/wv-window.rkt"
"../private/rgba.rkt"))
@title{wv-input}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[wv-input]
Typed input-element wrappers used by the window layer.
This module exports a family of classes derived from @racket[wv-element%]. Each
class represents one DOM input element and provides a typed @racket[get] method
together with a @racket[set!] method.
@section{Overview}
All classes in this module inherit from @racket[wv-element%].
Each input wrapper:
@itemlist[#:style 'compact
@item{is associated with one @racket[wv-window%]}
@item{is associated with one DOM element id}
@item{uses @racket[webview-set-value!] to write values}
@item{uses a type-specific @racket[webview-value*] function to read values}]
The classes do not add their own storage. They delegate directly to the
lower-level DOM/value API from @racketmodname[racket-webview]. Their accepted
argument shapes and result values therefore follow the contracts of those
lower-level functions.
@section{Common Structure}
All input wrapper classes have the same constructor shape:
@racketblock[
(new some-input% [window some-window] [element-id 'some-id])
]
where @racket[window] is the owning @racket[wv-window%] and
@racket[element-id] is the DOM id of the corresponding element.
Each class provides two public methods:
@itemlist[#:style 'compact
@item{@racket[get], returning the current typed value}
@item{@racket[set!], writing a new value through @racket[webview-set-value!]}]
@section{Class: wv-input/text%}
@defclass[wv-input/text% wv-element% ()]{
Wrapper for a text input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a text-input wrapper.
}
@defmethod[(get) (or/c string? boolean?)]{
Returns the current value by delegating to:
@racketblock[
(webview-value wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/number%}
@defclass[wv-input/number% wv-element% ()]{
Wrapper for a numeric input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a numeric-input wrapper.
}
@defmethod[(get) (or/c number? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/number wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/boolean%}
@defclass[wv-input/boolean% wv-element% ()]{
Wrapper for an input whose value is interpreted as a boolean.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a boolean-input wrapper.
}
@defmethod[(get) (or/c boolean? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/boolean wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/date%}
@defclass[wv-input/date% wv-element% ()]{
Wrapper for a date input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a date-input wrapper.
}
@defmethod[(get) (or/c g:date? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/date wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/time%}
@defclass[wv-input/time% wv-element% ()]{
Wrapper for a time input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a time-input wrapper.
}
@defmethod[(get) (or/c g:time? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/time wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/datetime%}
@defclass[wv-input/datetime% wv-element% ()]{
Wrapper for a datetime input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a datetime-input wrapper.
}
@defmethod[(get) (or/c g:datetime? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/datetime wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/range%}
@defclass[wv-input/range% wv-element% ()]{
Wrapper for a range input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a range-input wrapper.
}
@defmethod[(get) (or/c number? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/number wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/check%}
@defclass[wv-input/check% wv-element% ()]{
Wrapper for a checkbox input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a checkbox wrapper.
}
@defmethod[(get) (or/c boolean? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/boolean wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/radio%}
@defclass[wv-input/radio% wv-element% ()]{
Wrapper for a radio input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a radio-input wrapper.
}
@defmethod[(get) (or/c boolean? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/boolean wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}
@section{Class: wv-input/color%}
@defclass[wv-input/color% wv-element% ()]{
Wrapper for a color input element.
@defconstructor[([window (is-a?/c wv-window%)]
[element-id symbol?])]{
Creates a color-input wrapper.
}
@defmethod[(get) (or/c rgba? #f)]{
Returns the current value by delegating to:
@racketblock[
(webview-value/color wv element-id)
]
}
@defmethod[(set! [v (or/c symbol? string? number? boolean?
g:date? g:time? g:datetime? rgba?)])
any/c]{
Writes @racket[v] by delegating to:
@racketblock[
(webview-set-value! wv element-id v)
]
}
}

View File

@@ -1,69 +1,117 @@
#lang scribble/manual #lang scribble/manual
@(require scribble/racket @(require racket/base
scribble/example racket/class
scribble/core scribble/core
(for-label racket/base (for-label racket/base
racket/string racket/string
racket/class racket/class
racket/file) racket/file))
)
@defmodule[racket-webview] @title{wv-settings}
@title{Object-Oriented Interface: @racket[wv-settings%]}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[wv-settings]
Settings wrapper used by the webview library.
This module exports the @racket[wv-settings%] class, which provides a small
object-oriented interface over an @racket[ini] settings backend. Settings are
accessed relative to a context.
@section{Overview}
A @racket[wv-settings%] object combines:
@itemlist[#:style 'compact
@item{an @racket[ini] settings object}
@item{a @racket[symbol] identifying the settings context}]
Keys used in this class are symbols.
@section{Class: wv-settings%}
@defclass[wv-settings% object% ()]{ @defclass[wv-settings% object% ()]{
An OO wrapper around a settings backend implementing, e.g. @racket[simple-ini/class]. Wraps an @racket[ini] settings object for one context.
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?] The class delegates all reads and writes to the supplied @racket[ini] object.
@defconstructor[([ini (is-a?/c ini%)]
[wv-context symbol?])]{ [wv-context symbol?])]{
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 Creates a settings wrapper.
@item{@racket[(send ini get section key)]}
@item{@racket[(send ini get section key default)]} @racket[ini] must provide compatible @racket[get] and @racket[set!] methods.
@item{@racket[(send ini set! section key value)]} @racket[wv-context] is the context symbol used for contextual settings access.
}
@defmethod[(get [key symbol?] [default-value any/c] ...) any/c]{
Returns the value associated with @racket[key] in the current context.
If no default value is supplied, the call delegates to:
@racketblock[
(send ini get wv-context key)
]
If a default value is supplied, only the first extra argument is used, and the
call delegates to:
@racketblock[
(send ini get wv-context key default)
] ]
} }
@defmethod*[([(get [key symbol?]) any/c] @defmethod[(set! [key symbol?] [value any/c]) any/c]{
[(get [key symbol?] [default-value any/c]) any/c])]{
Returns the value associated with @racket[key] in the current section. Stores @racket[value] under @racket[key] in the current context.
Returns @tt{default-value} (when supplied) if the key is not found.
Delegates to:
@racketblock[
(send ini set! wv-context key value)
]
} }
@defmethod*[([(set! [key symbol?] [value any/c]) this])]{ @defmethod[(get/global [key symbol?] [default-value any/c] ...) any/c]{
Stores @racket[value] under @racket[key] in the current section.
Returns the value associated with @racket[key] in the global settings section.
If no default value is supplied, the call delegates to:
@racketblock[
(send ini get 'global key)
]
If a default value is supplied, only the first extra argument is used, and the
call delegates to:
@racketblock[
(send ini get 'global key default)
]
} }
@defmethod*[([(get/global [key symbol?]) any/c] @defmethod[(set/global! [key symbol?] [value any/c]) any/c]{
[(get/global [key symbol?] [default-value any/c]) any/c])]{
Returns the value associated with @racket[key] in the global section. Stores @racket[value] under @racket[key] in the global settings section.
This method delegates to @tt{(send ini get 'global key default-value)}.
Delegates to:
@racketblock[
(send ini set! 'global key value)
]
} }
@defmethod*[([(set/global! [key symbol?] [value any/c]) this])]{ @defmethod[(clone [context symbol?]) wv-settings%]{
Stores @racket[value] under @racket[key] in the global section.
Delegates to @tt{(send ini set! 'global key value)}. Creates a new @racket[wv-settings%] object sharing the same @racket[ini]
backend but using @racket[context] as its context.
} }
@defmethod*[([(clone [context any/c]) (is-a?/c wv-settings%)])]{ @defmethod[(context) symbol?]{
Creates a new @racket[wv-settings%] object using the same
@racket[ini] backend but with another section identifier.
It is intended to support cheap creation of per-section settings views.
}
@defmethod*[([(context) symbol?])]{ Returns the current context symbol.
Returns the section identifier associated with this settings object.
} }
} }

View File

@@ -1,256 +1,457 @@
#lang scribble/manual #lang scribble/manual
@(require scribble/racket @(require racket/base
scribble/example racket/class
scribble/core scribble/core
(for-label racket/base (for-label racket/base
racket/string
racket/class racket/class
racket/path racket/file
net/url net/url
"../private/wv-context.rkt" "../private/wv-context.rkt"
"../private/wv-settings.rkt")) "../private/wv-settings.rkt"
"../private/wv-element.rkt"
"../private/wv-input.rkt"
"../private/rgba.rkt"))
@title{wv-window}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[wv-window] @defmodule[wv-window]
@title{Object-Oriented Interface: @racket[wv-window%]} Window abstraction built on top of @racketmodname[racket-webview].
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
This module exports the @racket[wv-window%] class and re-exports the APIs from
@racketmodname[wv-element], @racketmodname[wv-input], @racketmodname[rgba], and
@racketmodname[wv-settings].
@section{Overview}
A @racket[wv-window%] object represents one webview window.
A window belongs to a context, loads one HTML path within that context, manages
its own title and geometry, dispatches incoming events, and provides access to
element wrappers through @racket[element].
The class also provides synchronous wrappers around the asynchronous dialog
interface by using continuations internally.
@section{Class: wv-window%}
@defclass[wv-window% object% ()]{ @defclass[wv-window% object% ()]{
An OO wrapper around a single webview window. Represents one window in a webview context.
A @racket[wv-window%] object owns one low-level window created with
@racket[webview-create], keeps a per-window settings view, and offers methods for
window management, DOM element access, event binding, file dialogs, and message boxes.
The class is designed so that subclasses can override event methods such as The class wraps a lower-level window created through
@racket[moved], @racket[resized], @racket[page-loaded], @racket[js-event], @racketmodname[racket-webview] and connects it to the surrounding class-based
@racket[navigation-request], @racket[can-close?], and @racket[closed]. API.
@defconstructor[([parent (or/c #f (is-a?/c wv-window%)) #f] @defconstructor[([parent (or/c #f (is-a?/c wv-window%)) #f]
[wv-context (is-a?/c wv-context%) (if (eq? parent #f) [wv-context (if (eq? parent #f)
(error "wv context is mandatory") (error "wv context is mandatory")
(get-field wv-context parent))] (get-field wv-context parent))]
[html-path path-string? (error "html path is mandatory")] [html-path path-string?]
[settings (is-a?/c wv-settings%) [settings (send wv-context settings
(send wv-context settings
(string->symbol (format "~a" html-path)))] (string->symbol (format "~a" html-path)))]
[title string? "Racket Webview Window"] [title string? "Racket Webview Window"]
[width (or/c #f exact-integer?) #f] [width (or/c exact-integer? #f) #f]
[height (or/c #f exact-integer?) #f] [height (or/c exact-integer? #f) #f]
[x (or/c #f exact-integer?) #f] [x (or/c exact-integer? #f) #f]
[y (or/c #f exact-integer?) #f] [y (or/c exact-integer? #f) #f])]{
[wv any/c #f])]{
Creates a new window.
The @racket[wv-context] argument supplies the underlying webview context. Creates a window.
If @racket[parent] is given, the default value of @racket[wv-context] is taken from it.
The @racket[html-path] argument identifies the HTML resource to open.
The default @racket[settings] object is derived from @racket[wv-context] using
@racket[html-path] as per-window settings key.
The constructor creates the low-level window using @racket[webview-create] and then The constructor requires @racket[html-path]. If @racket[parent] is @racket[#f],
calls @racket[init-size] to restore position and size from settings, falling back to @racket[wv-context] is mandatory. If a parent window is supplied,
constructor arguments or internal defaults. @racket[wv-context] defaults to the context of that parent.
The default value of @racket[settings] is obtained as:
@racketblock[
(send wv-context settings (string->symbol (format "~a" html-path)))
]
After construction, the class:
@itemlist[#:style 'compact
@item{creates the underlying webview window}
@item{installs the internal event handler}
@item{sets the title}
@item{initializes position and size from settings or defaults}]
If a parent is supplied, the underlying lower-level parent window is passed to
@racket[webview-create].
} }
@defmethod*[([(context) (is-a?/c wv-context%)])]{ @defmethod[(context) any/c]{
Returns the window context object associated with this window.
Returns the window context object supplied at construction.
} }
@defmethod*[([(win-context) symbol?])]{ @defmethod[(win-context) symbol?]{
Returns the symbolic context identifier derived from @racket[html-path].
This value is typically used as the settings section for the window. Returns the settings context key for this window.
The value is computed as:
@racketblock[
(string->symbol (format "~a" html-path))
]
} }
@defmethod*[([(info) hash?])]{ @defmethod[(info) hash?]{
Returns a hash table with administrative information about the window.
The hash contains entries for at least @racket['wv-context], @racket['wv], Returns administrative information for the window as a hash table.
The returned hash contains the keys @racket['wv-context], @racket['wv],
@racket['html-path], and @racket['elements]. @racket['html-path], and @racket['elements].
} }
@defmethod*[([(element [id symbol?]) any/c] @defmethod[(element [id symbol?] [type symbol?] ...) any/c]{
[(element [id symbol?] [type symbol?]) any/c])]{
Returns the wrapper object for the DOM element with identifier @racket[id].
If the element has not been wrapped before, a new wrapper object is created and cached. Returns the wrapper object associated with the DOM element identified by
When @racket[type] is omitted, the method attempts to determine the HTML input type via @racket[id].
JavaScript. Depending on the resolved type, it creates an instance of a more specific
wrapper class such as @racket[wv-input/text%], @racket[wv-input/date%], Element wrappers are cached in an internal weak hash table. If no wrapper exists
@racket[wv-input/number%], or falls back to @racket[wv-element%]. yet, one is created and cached.
If no explicit @racket[type] is supplied, the method asks the browser for the
element's @tt{type} attribute. Depending on the resulting type, one of the
following classes is instantiated:
@itemlist[#:style 'compact
@item{@racket[wv-input/text%] for @racket['text]}
@item{@racket[wv-input/date%] for @racket['date]}
@item{@racket[wv-input/time%] for @racket['time]}
@item{@racket[wv-input/datetime%] for @racket['datetime-local]}
@item{@racket[wv-input/range%] for @racket['range]}
@item{@racket[wv-input/number%] for @racket['number]}
@item{@racket[wv-input/check%] for @racket['checkbox]}
@item{@racket[wv-input/radio%] for @racket['radio]}
@item{@racket[wv-input/color%] for @racket['color]}
@item{@racket[wv-element%] otherwise}]
Newly created wrappers receive this window through the @racket[window] init
argument and the element id through @racket[element-id].
} }
@defmethod*[([(moved [x exact-integer?] [y exact-integer?]) void?])]{ @defmethod[(moved [xx exact-integer?] [yy exact-integer?]) any/c]{
Event hook called when the window has moved.
The default implementation stores the new position in the settings object. Updates the internal position and stores the new coordinates in
Subclasses may override this method. @racket[settings] under @racket['x] and @racket['y].
} }
@defmethod*[([(resized [width exact-integer?] [height exact-integer?]) void?])]{ @defmethod[(resized [w exact-integer?] [h exact-integer?]) any/c]{
Event hook called when the window has been resized.
The default implementation stores the new size in the settings object. Updates the internal size and stores the new dimensions in
Subclasses may override this method. @racket[settings] under @racket['width] and @racket['height].
} }
@defmethod*[([(window-state-changed [state any/c]) any/c])]{ @defmethod[(window-state-changed [st symbol?]) any/c]{
Event hook called when the native window state changes, for example after show,
hide, or maximize notifications. The default implementation returns @racket[#t].
}
@defmethod*[([(page-loaded [ok? any/c]) any/c])]{ Called when the underlying window state changes.
Event hook called after the page load completes. The argument indicates whether the
load succeeded. The default implementation returns @racket[#t].
}
@defmethod*[([(can-close?) any/c])]{
Event hook queried before closing the window.
If this method returns a true value, the default event handler closes the window.
The default implementation returns @racket[#t]. The default implementation returns @racket[#t].
} }
@defmethod*[([(closed) any/c])]{ @defmethod[(page-loaded [oke any/c]) any/c]{
Event hook called after the window has been closed.
Called when a @racket['page-loaded] event is received.
The default implementation returns @racket[#t]. The default implementation returns @racket[#t].
} }
@defmethod*[([(js-event [js-event hash?]) any/c])]{ @defmethod[(can-close?) any/c]{
Handles a JavaScript-originated event.
The default implementation looks up the wrapped element referenced by the event and Called when a @racket['can-close?] event is received.
forwards the event to that element's dispatcher. If no matching element is known,
it returns @racket['wv-unhandled-js-event]. Subclasses may override this for If this method returns a true value, the window closes itself by calling
application-specific dispatch. @racket[close].
The default implementation returns @racket[#t].
} }
@defmethod*[([(navigation-request [type symbol?] [url url?]) (or/c 'internal 'external)])]{ @defmethod[(closed) any/c]{
Handles a navigation request.
If the requested URL starts with the context base URL, the window navigates internally Called when a @racket['closed] event is received.
using @racket[webview-set-url!] and returns @racket['internal]. Otherwise the URL is
opened externally using @racket[send-url] and the method returns @racket['external]. The default implementation returns @racket[#t].
} }
@defmethod*[([(add-class! [selector-or-id string?] [class string?]) this])]{ @defmethod[(js-event [js-event hash?]) any/c]{
Adds the CSS class @racket[class] to the elements selected by @racket[selector-or-id].
Delegates to @racket[webview-add-class!]. Dispatches a JavaScript event received from the browser.
The incoming hash is expected to contain at least the keys @racket['evt],
@racket['id], and optionally @racket['data]. If the referenced element exists in
the internal element cache, the event is dispatched to that element by calling
its @racket[dispatch-event] method.
If no matching element wrapper exists, the method returns
@racket['wv-unhandled-js-event].
} }
@defmethod*[([(remove-class! [selector-or-id string?] [class string?]) this])]{ @defmethod[(navigation-request [type symbol?] [url url?]) (or/c 'internal 'external)]{
Removes the CSS class @racket[class] from the elements selected by @racket[selector-or-id].
Delegates to @racket[webview-remove-class!]. Handles a navigation request event.
If the requested URL starts with the context base URL, the request is treated as
internal, loaded into the current window, and @racket['internal] is returned.
Otherwise the URL is opened externally using @racket[send-url], and
@racket['external] is returned.
} }
@defmethod*[([(devtools) this])]{ @defmethod[(add-class! [selector-or-id (or/c symbol? string?)]
Opens or activates the developer tools for the underlying webview. [cl (or/c symbol? string? list?)])
(is-a?/c wv-window%)]{
Adds one or more CSS classes to the selected elements and returns this window.
} }
@defmethod*[([(move [x exact-integer?] [y exact-integer?]) this])]{ @defmethod[(remove-class! [selector-or-id (or/c symbol? string?)]
Moves the native window to the given screen coordinates. [cl (or/c symbol? string? list?)])
(is-a?/c wv-window%)]{
Removes one or more CSS classes from the selected elements and returns this
window.
} }
@defmethod*[([(resize [width exact-integer?] [height exact-integer?]) this])]{ @defmethod[(devtools) (is-a?/c wv-window%)]{
Resizes the native window to the given dimensions.
Opens the developer tools and returns this window.
} }
@defmethod*[([(close) this])]{ @defmethod[(move [x exact-integer?] [y exact-integer?]) (is-a?/c wv-window%)]{
Closes the native window.
Moves the window and returns this window.
} }
@defmethod*[([(bind! [selector string?] @defmethod[(resize [w exact-integer?] [h exact-integer?]) (is-a?/c wv-window%)]{
[events (or/c symbol? (listof symbol?))]
[callback procedure?]) (listof any/c)])]{
Binds one or more DOM events to all elements matched by @racket[selector].
This method delegates the JavaScript side of the binding to @racket[webview-bind!], Resizes the window and returns this window.
creates or reuses wrapper objects for the matched elements, and installs
@racket[callback] as event callback on each wrapper. The return value is the list of
matched element wrapper objects.
} }
@defmethod*[([(unbind! [selector string?] @defmethod[(close) (is-a?/c wv-window%)]{
[events (or/c symbol? (listof symbol?))]) any/c])]{
Removes previously installed bindings for the selected elements and events. Closes the window and returns this window.
This method delegates to @racket[webview-unbind!] and removes callbacks from the
corresponding cached wrapper objects.
} }
@defmethod*[([(file-dialog-done [flag symbol?] @defmethod[(bind! [selector (or/c symbol? string?)]
[events (or/c symbol? list?)]
[callback procedure?])
list?]{
Binds one or more DOM events.
The method first delegates to @racket[webview-bind!]. For each returned binding,
it connects the corresponding element wrapper to @racket[callback].
If @racket[events] is a symbol, it is treated as a single-element list.
The result is the list produced by mapping over the binding items. In practice,
this yields the corresponding cached element wrapper objects.
}
@defmethod[(unbind! [selector (or/c symbol? string?)]
[events (or/c symbol? list?)])
list?]{
Removes one or more DOM event bindings.
The method first delegates to @racket[webview-unbind!], then disconnects the
corresponding callbacks from cached element wrappers.
If an element wrapper no longer has any event callbacks, it is removed from the
internal cache.
The returned value is the list produced by @racket[webview-unbind!].
}
@defmethod[(set-title! [title string?]) any/c]{
Sets the window title.
}
@defmethod[(file-dialog-done [flag symbol?]
[file any/c] [file any/c]
[dir any/c] [dir any/c]
[filter any/c]) void?])]{ [filter any/c])
Continuation hook used internally to resume a pending file or directory dialog. any/c]{
Applications normally do not call this method directly.
Internal continuation callback used by file and directory dialogs.
The default implementation resumes the pending continuation stored by the window.
} }
@defmethod*[([(choose-dir [title string?] [base-dir (or/c #f path-string?)]) @defmethod[(choose-dir [title string?] [base-dir (or/c path-string? #f)])
(or/c 'showing path-string? #f)])]{ (or/c 'showing path-string? #f)]{
Shows a directory chooser.
If the chooser is successfully shown, the method initially returns @racket['showing] Shows a directory chooser dialog.
and later resumes with the selected directory path or @racket[#f] when the dialog is
canceled. If the dialog cannot be shown, it returns @racket[#f]. At most one file or directory dialog can be active at a time. Attempting to open
Only one file or directory dialog can be active at a time. another one raises an exception.
If @racket[base-dir] is @racket[#f], the user's home directory is used.
If the underlying dialog could not be started, the method returns @racket[#f].
If the dialog is shown successfully, the method returns @racket['showing] first
and later resumes through the internal continuation mechanism.
When resumed after completion:
@itemlist[#:style 'compact
@item{the selected directory is returned on success}
@item{@racket[#f] is returned if the dialog was canceled}]
} }
@defmethod*[([(file-open [title string?] @defmethod[(file-open [title string?]
[base-dir (or/c #f path-string?)] [base-dir (or/c path-string? #f)]
[filters any/c]) [filters any/c])
(or/c 'showing list? #f)])]{ (or/c 'showing list? #f)]{
Shows a file-open dialog.
If the dialog is accepted, the resumed result is a list whose contents are derived from Shows an open-file dialog.
the low-level file dialog result. If the dialog is canceled or cannot be shown, the
result is @racket[#f]. Only one file or directory dialog can be active at a time. At most one file or directory dialog can be active at a time. Attempting to open
another one raises an exception.
If @racket[base-dir] is @racket[#f], the user's home directory is used.
If the underlying dialog could not be started, the method returns @racket[#f].
If the dialog is shown successfully, the method returns @racket['showing] first
and later resumes through the internal continuation mechanism.
When resumed after completion:
@itemlist[#:style 'compact
@item{on success, the method returns @racket[(cdr r)] from the internal dialog result}
@item{on cancellation, the method returns @racket[#f]}]
This matches the current source code exactly.
} }
@defmethod*[([(file-save [title string?] @defmethod[(file-save [title string?]
[base-dir (or/c #f path-string?)] [base-dir (or/c path-string? #f)]
[filters any/c]) [filters any/c])
(or/c 'showing list? #f)])]{ (or/c 'showing list? #f)]{
Shows a file-save dialog.
If the dialog is accepted, the resumed result is a list whose contents are derived from Shows a save-file dialog.
the low-level file dialog result. If the dialog is canceled or cannot be shown, the
result is @racket[#f]. Only one file or directory dialog can be active at a time. At most one file or directory dialog can be active at a time. Attempting to open
another one raises an exception.
If @racket[base-dir] is @racket[#f], the user's home directory is used.
If the underlying dialog could not be started, the method returns @racket[#f].
If the dialog is shown successfully, the method returns @racket['showing] first
and later resumes through the internal continuation mechanism.
When resumed after completion:
@itemlist[#:style 'compact
@item{on success, the method returns @racket[(cdr r)] from the internal dialog result}
@item{on cancellation, the method returns @racket[#f]}]
This matches the current source code exactly.
} }
@defmethod*[([(message-done [event symbol?]) void?])]{ @defmethod[(message-done [evt symbol?]) any/c]{
Continuation hook used internally to resume a pending message box.
Applications normally do not call this method directly. Internal continuation callback used by message boxes.
The default implementation resumes the pending message-box continuation stored by
the window.
} }
@defmethod*[([(message [type symbol?] @defmethod[(message [type symbol?]
[title string?] [title string?]
[message string?] [message string?]
[#:sub submessage string? ""]) [#:sub submessage string? ""])
(or/c 'showing symbol? #f)])]{ (or/c 'showing symbol? #f)]{
Shows a native message box.
If the message box is shown successfully, the method initially returns @racket['showing] Shows a message box.
and later resumes with the button result, such as @racket['msgbox-ok],
@racket['msgbox-cancel], @racket['msgbox-yes], or @racket['msgbox-no]. At most one message box can be active at a time. Attempting to show another one
If the message box cannot be shown, the method returns @racket[#f]. raises an exception.
Only one message box can be active at a time.
If the underlying dialog could not be started, the method returns @racket[#f].
If the dialog is shown successfully, the method returns @racket['showing] first
and later resumes through the internal continuation mechanism.
When resumed after completion, the result is the event symbol returned by the
lower layer, such as the symbols corresponding to @tt{ok}, @tt{cancel},
@tt{yes}, or @tt{no}.
} }
@defmethod*[([(init-size) void?])]{ @defmethod[(init-size) any/c]{
Initializes the window position and size from the settings object.
If no persisted values are available, constructor arguments or module defaults are used. Initializes the window position and size.
The method reads @racket['x], @racket['y], @racket['width], and
@racket['height] from @racket[settings]. If a value is missing, the constructor
argument is used if present; otherwise a default is chosen.
The resulting geometry is then applied through @racket[move] and @racket[resize].
}
} }
} @section{Events}
@defproc[(root-file-not-found-handler [standard-file path-string?] The window installs an internal event handler when it is created.
[not-found-handler procedure?])
This handler reacts to the following event kinds produced by the lower layer:
@itemlist[#:style 'compact
@item{@racket['resize], forwarded to @racket[resized]}
@item{@racket['move], forwarded to @racket[moved]}
@item{@racket['page-loaded], forwarded to @racket[page-loaded]}
@item{@racket['show], @racket['hide], and @racket['maximize], forwarded to
@racket[window-state-changed]}
@item{@racket['can-close?], causing @racket[can-close?] to be queried}
@item{@racket['closed], forwarded to @racket[closed]}
@item{@racket['js-evt], decoded and forwarded to @racket[js-event]}
@item{@racket['navigation-request], forwarded to @racket[navigation-request]}
@item{file-dialog completion events, forwarded to @racket[file-dialog-done]}
@item{message-box completion events, forwarded to @racket[message-done]}]
Unhandled events are printed to the console by the current implementation.
@section{Function: root-file-not-found-handler}
@defproc[(root-file-not-found-handler [standard-file string?]
[not-found-handler procedure?] ...)
procedure?]{ procedure?]{
Creates a file-resolution procedure that can be used as a fallback handler for a webview
context.
The returned procedure redirects requests for @tt{"/"} and, when needed, Creates a file-not-found handler for use with file serving.
@tt{"/index.html"} to @racket[standard-file]. For other missing files it either returns
the unresolved path unchanged or delegates to @racket[not-found-handler] when one is The returned procedure has the shape:
supplied.
@racketblock[
(lambda (file base-path path) ...)
]
Its behavior is as follows:
@itemlist[#:style 'compact
@item{if @racket[file] is @tt{"/"}, it retries using @racket[standard-file]}
@item{if @racket[file] is @tt{"/index.html"}, it falls back to
@racket[standard-file] when the target does not exist}
@item{if the requested path exists, it is returned}
@item{otherwise, if an extra @racket[not-found-handler] was supplied, that
handler is called}
@item{if no extra handler was supplied, the original path is returned}]
Only the first optional @racket[not-found-handler] is used.
} }
@defthing[webview-version any/c]{ @section{Re-exports}
The version identifier exported by the underlying webview binding.
} This module also re-exports the public APIs from:
@itemlist[#:style 'compact
@item{@racketmodname[wv-element]}
@item{@racketmodname[wv-input]}
@item{@racketmodname[rgba]}
@item{@racketmodname[wv-settings]}]
It also re-exports @racket[webview-version].