diff --git a/main.rkt b/main.rkt index 7449c53..97d324f 100644 --- a/main.rkt +++ b/main.rkt @@ -4,10 +4,14 @@ (require "private/wv-context.rkt") (require "private/wv-window.rkt") (require "private/wv-dialog.rkt") +(require "private/wv-element.rkt") +(require "private/wv-input.rkt") (provide (all-from-out "private/wv-context.rkt" "private/wv-window.rkt" "private/wv-dialog.rkt" + "private/wv-element.rkt" + "private/wv-input.rkt" ) webview-set-loglevel webview-version diff --git a/private/racket-webview-qt.rkt b/private/racket-webview-qt.rkt index 74cd743..b8a6460 100644 --- a/private/racket-webview-qt.rkt +++ b/private/racket-webview-qt.rkt @@ -164,8 +164,11 @@ ([eq? os 'linux] (error (format (string-append "Cannot load ~a.\n" - "Make sure you installed Qt6on your system\n" - "e.g. on debian 'sudo apt install libqt6webenginewidgets6'\n" + "Make sure you installed Qt6 on your system\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" "Exception:\n\n~a") ffi-library exp))) @@ -260,9 +263,7 @@ ) (define-cstruct _rkt_version_t - ([qt-major _int] - [qt-minor _int] - [qt-patch _int] + ( [api-major _int] [api-minor _int] [api-patch _int] @@ -711,16 +712,12 @@ (define (rkt-webview-version) (let ((d (rkt_webview_version))) (let ((v (union-ref (rkt_data_t-data d) 0))) - (let ((qt-major (rkt_version_t-qt-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)) + (let ((api-major (rkt_version_t-api-major v)) (api-minor (rkt_version_t-api-minor v)) (api-patch (rkt_version_t-api-patch v)) ) (rkt_webview_free_data d) - (list (list 'webview-c-api api-major api-minor api-patch) - (list 'qt qt-major qt-minor qt-patch)) + (list (list 'webview-c-api api-major api-minor api-patch)) ) ) ) diff --git a/private/racket-webview.rkt b/private/racket-webview.rkt index 5ae9181..f797406 100644 --- a/private/racket-webview.rkt +++ b/private/racket-webview.rkt @@ -663,7 +663,7 @@ p))))) -(define (webview-call-js wv js) +(define/contract (webview-call-js wv js) (-> wv-win? string? (or/c string? list? boolean? hash?)) (let ((result (rkt-webview-call-js (wv-win-handle wv) js))) ;(displayln result) diff --git a/private/wv-element.rkt b/private/wv-element.rkt index 751b571..0155349 100644 --- a/private/wv-element.rkt +++ b/private/wv-element.rkt @@ -63,28 +63,28 @@ (define/public (unset-style! 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)) - (define (attr attr) + (define/public (attr attr) (webview-attr wv element-id attr)) - (define (attr/number attr) + (define/public (attr/number attr) (webview-attr/number wv element-id attr)) - (define (attr/symbol attr) + (define/public (attr/symbol attr) (webview-attr/symbol wv element-id attr)) - (define (attr/boolean attr) + (define/public (attr/boolean attr) (webview-attr/boolean wv element-id attr)) - (define (attr/date attr) + (define/public (attr/date attr) (webview-attr/date wv element-id attr)) - (define (attr/time attr) + (define/public (attr/time attr) (webview-attr/time wv element-id attr)) - (define (attr/datetime attr) + (define/public (attr/datetime attr) (webview-attr/datetime wv element-id attr)) (super-new) diff --git a/scrbl/racket-webview.scrbl b/scrbl/racket-webview.scrbl index 7fca68f..1c9606d 100644 --- a/scrbl/racket-webview.scrbl +++ b/scrbl/racket-webview.scrbl @@ -1,139 +1,68 @@ #lang scribble/manual -@title{High-Level Racket Interface for Webviews} +@(require racket/base + scribble/core) + +@title{racket-webview} @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @defmodule[racket-webview] -@section{Overview} +Higher-level interface built on top of @racketmodname[racket-webview-qt]. -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. +This module provides a structured programming model around the lower-level +webview bindings. It introduces contexts, a local HTTPS server, JSON-based +event handling, and DOM and JavaScript utilities. @section{Architecture} -This module sits above the low-level FFI wrapper and provides a more idiomatic -Racket API. - -The stack is organized as follows: +The module builds on the lower-level bindings from +@racketmodname[racket-webview-qt]. It adds: @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}} -] + @item{structured Racket values instead of raw strings} + @item{JSON decoding of events and JavaScript results} + @item{a local HTTPS server per context} + @item{convenience functions for DOM manipulation}] -This layer introduces two main abstractions: +A context encapsulates a native webview context together with a local HTTPS +server. Windows are created within a context and communicate through events and +JavaScript calls. -@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. -} +@section{Contexts} @defproc[(webview-new-context [file-getter procedure?] [#:boilerplate-js boilerplate-js string? - (webview-default-boilerplate-js)]) + (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: +Creates a new context. @itemlist[#:style 'compact - @item{creates a fresh context structure} + @item{starts a local HTTPS server on a dynamically chosen port} @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} -] + @item{creates a native context} + @item{installs the provided JavaScript boilerplate}] -The @racket[boilerplate-js] argument supplies JavaScript that is injected into -pages loaded within the native context. +The @racket[file-getter] procedure maps request paths to files. -The returned value is a @racket[wv-context] structure. +Certificate files are removed automatically when the context is garbage +collected. } @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}. +Returns the base URL of the context. + +This URL can be used to construct URLs from relative path information. } -@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-context? [v any/c]) boolean?]{ +Recognizes context values. } -@defproc[(wv-win-window-nr [wv wv-win?]) exact-integer?]{ -Returns the native numeric window identifier of @racket[wv]. -} +@section{Windows} @defproc[(webview-create [context wv-context?] @@ -142,75 +71,36 @@ Returns the native numeric window identifier of @racket[wv]. [#:parent parent (or/c wv-win? #f) #f]) wv-win?]{ -Creates a new webview window inside the given context. - -Arguments: +Creates a window in @racket[context], navigates it to @racket[url-path], and +returns a window handle. @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} + @item{a native window is created} + @item{an event handler is installed} + @item{the context security token is applied} + @item{navigation is performed via the local server}] + +The callback is invoked as: + +@racketblock[ +(event-callback wv evt) ] -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!]. +where @racket[evt] is a parsed event hash table. -The callback receives two arguments: +If @racket[parent] is provided, the new window becomes a child of that window. +} -@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[(wv-win-window-nr [wv wv-win?]) exact-integer?]{ +Returns the native window number. } @defproc[(webview-close [wv wv-win?]) symbol?]{ -Closes the webview. - -Returns @racket['oke]. +Closes the window. Always 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. +Opens developer tools for the window. } @defproc[(webview-move [wv wv-win?] [x number?] [y number?]) symbol?]{ @@ -221,39 +111,70 @@ Moves the window. 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-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?]{Shows the window in normal state.} +@defproc[(webview-maximize [wv wv-win?]) symbol?]{Maximizes the window.} +@defproc[(webview-minimize [wv wv-win?]) symbol?]{Minimizes the window.} @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{Navigation and Content} + +@defproc[(webview-set-url! [wv wv-win?] [url (or/c string? url?)]) + symbol?]{ + +Loads @racket[url]. } -@section{Message Boxes and File Dialogs} +@defproc[(webview-navigate! [wv wv-win?] [place string?]) symbol?]{ + +Navigates by assigning @tt{window.location} via JavaScript. +} + +@defproc[(webview-base-url [wv wv-win?]) url?]{ +Returns the context base URL as a URL value. +} + +@defproc[(webview-set-html! [wv wv-win?] [html (or/c string? xexpr?)]) + symbol?]{ + +Replaces the current document contents. + +X-expressions are converted to strings before being passed to JavaScript. +} + +@defproc[(webview-set-title! [wv wv-win?] [title string?]) symbol?]{ +Sets the window title. +} + +@section{JavaScript} + +@defproc[(webview-run-js [wv wv-win?] [js string?]) symbol?]{ +Evaluates JavaScript. +} + +@defproc[(webview-call-js-result? [x any/c]) boolean?]{ +Recognizes lower-level JavaScript call results. +} + +@defproc[(webview-call-js [wv wv-win?] [js string?]) + (or/c string? list? boolean? hash?)]{ + +Evaluates JavaScript and returns a decoded value. + +If the lower-level call fails or the result does not match the expected +structure, an exception is raised. +} + + +@section{Dialogs} + +Dialog functions return immediately. Results are delivered asynchronously via +events. @defproc[(webview-messagebox [wv wv-win?] @@ -263,12 +184,10 @@ Sets the native window title. [#:sub submessage string? ""]) symbol?]{ -Shows a native message box. +Shows a 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]. +The user response is delivered asynchronously via events. If dismissed or +canceled, this is indicated through the event. } @defproc[(webview-choose-dir @@ -277,7 +196,10 @@ to be one of the supported message box kinds, such as [base-dir (or/c path? string?)]) symbol?]{ -Opens a directory chooser dialog. +Shows a directory chooser. + +The selected directory is delivered asynchronously via events. If canceled, +this is indicated through the event. } @defproc[(webview-file-open @@ -287,9 +209,10 @@ Opens a directory chooser dialog. [permitted-exts (or/c wv-permitted-exts? wv-list-of-permitted-exts?)]) symbol?]{ -Opens a native file-open dialog. +Shows an open-file dialog. -The @racket[permitted-exts] value is converted to a Qt file filter string. +The selected file is delivered asynchronously via events. If canceled, this is +indicated through the event. } @defproc[(webview-file-save @@ -299,52 +222,32 @@ The @racket[permitted-exts] value is converted to a Qt file filter string. [permitted-exts (or/c wv-permitted-exts? wv-list-of-permitted-exts?)]) symbol?]{ -Opens a native file-save dialog. +Shows a save-file dialog. -The @racket[permitted-exts] value is converted to a Qt file filter string. +The file to be saved is delivered asynchronously via events. If canceled, this +is indicated through the event. } -@section{Permitted Extensions} -The file dialog helpers use @racket[wv-permitted-exts] values to describe file -type filters. +@section{DOM Interaction} -@defstruct*[wv-permitted-exts - ([name string?] - [exts wv-permitted-exts-exts?])]{ -Represents a named file filter. +@subsection{Selectors and Element Identifiers} -The @racket[name] field is the human-readable filter name. +Many functions accept either an element identifier or a CSS selector. -The @racket[exts] field is a list of filename extensions represented as -symbols. -} +If a symbol is provided, it is interpreted as an element id. The symbol is +converted to a string and prefixed with @tt{"#"}. -@defproc[(wv-permitted-exts-exts? [v any/c]) boolean?]{ -Returns whether @racket[v] is a valid list of extension symbols. -} +If a string is provided, it is used directly as a CSS selector. -@defproc[(wv-permitted-exts-name? [v any/c]) boolean?]{ -Returns whether @racket[v] is a valid filter name. -} +@itemlist[#:style 'compact + @item{a symbol refers to a single element by id} + @item{a string may refer to one or more elements} + @item{CSS selector semantics are those of the browser engine}] -@defproc[(wv-list-of-permitted-exts? [v any/c]) boolean?]{ -Returns whether @racket[v] is a list of valid @racket[wv-permitted-exts] -values. -} +Functions apply their effect to all matched elements. -@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. +@subsection{Event Binding} @defproc[(webview-bind! [wv wv-win?] @@ -352,14 +255,12 @@ the Racket side. [event (or/c symbol? list-of-symbol?)]) list?]{ -Binds one or more DOM events to elements selected by @racket[selector]. +Installs one or more DOM event bindings. If @racket[selector] is a symbol, it is interpreted as an element id and -rewritten as a CSS id selector. +converted to a CSS selector by prefixing @tt{"#"}. -If @racket[event] is a symbol, it is treated as a single event name. - -The result is a list describing the created bindings. +The result is a list describing the installed bindings. } @defproc[(webview-unbind! @@ -368,149 +269,105 @@ The result is a list describing the created bindings. [event (or/c symbol? list-of-symbol?)]) list?]{ -Removes previously established DOM event bindings. +Removes one or more DOM event bindings. -Its arguments and return value follow the same conventions as -@racket[webview-bind!]. +Selector handling is the same as for @racket[webview-bind!]. The result is a +list describing the removed bindings. } -@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} +@subsection{DOM Values} @defproc[(webview-set-value! [wv wv-win?] [id symbol?] - [val (or/c symbol? - string? - number? - boolean? - g:date? - g:time? - g:datetime? - rgba?)]) + [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. +Sets the value of the element identified by @racket[id] using JavaScript. -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. +For checkboxes and radio buttons, the checked state is updated. Other elements +receive a string value. Dates, times, datetimes, and colors are converted to +strings before being passed to JavaScript. } @defproc[(webview-value [wv wv-win?] [id symbol?]) (or/c string? boolean?)]{ -Returns the current value of the element with the given id. +Returns the value of the element identified by @racket[id]. -For checkboxes and radio buttons, the returned value reflects the checked state. -For other elements, the result is the string value. +For checkboxes and radio buttons, the result reflects the checked state. Other +elements return their @tt{value} property. } @defproc[(webview-value/boolean [wv wv-win?] [id symbol?]) (or/c boolean? #f)]{ -Returns the current value converted to a boolean. + +Returns the 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. + +Returns the 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. + +Returns the 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. + +Returns the 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. + +Returns the 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. + +Returns the 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. + +Returns the value converted to a color. } -@section{Class Helpers} +@subsection{Classes, Styles} @defproc[(webview-add-class! [wv wv-win?] - [id-or-selector (or/c symbol? string?)] + [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]. +Adds one or more CSS classes using JavaScript. -A symbol selector is interpreted as an element id. +If @racket[selector] is a symbol, it is treated as an element id. If it is a +string, it is used directly as a CSS selector. + +The effect is applied to all matched elements. } @defproc[(webview-remove-class! [wv wv-win?] - [id-or-selector (or/c symbol? string?)] + [selector (or/c symbol? string?)] [class (or/c symbol? string? list?)]) hash?]{ -Removes one or more CSS classes from all selected elements. -} +Removes one or more CSS classes using JavaScript. -@section{Style Helpers} +Selector handling is the same as for @racket[webview-add-class!]. +} @defproc[(webview-set-style! [wv wv-win?] @@ -518,25 +375,9 @@ Removes one or more CSS classes from all selected elements. [style-entries (or/c kv? list-of-kv?)]) hash?]{ -Sets one or more inline CSS style properties on the selected elements. +Sets inline style properties using JavaScript. -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. +The effect is applied to all elements matched by @racket[selector]. } @defproc[(webview-unset-style! @@ -545,10 +386,24 @@ If @racket[selector] is a string selector, the result is a list of [style-entries (or/c symbol? list-of-symbol?)]) hash?]{ -Clears one or more inline style properties from the selected elements. +Clears inline style properties using JavaScript. + +The effect is applied to all elements matched by @racket[selector]. } -@section{Attribute Helpers} +@defproc[(webview-get-style + [wv wv-win?] + [selector (or/c symbol? string?)] + [styles (or/c symbol? list-of-symbol?)]) + (or/c list? hash?)]{ + +Retrieves computed style values. + +If @racket[selector] is a symbol, the result for that single element is returned +directly. Otherwise the result covers all matched elements. +} + +@subsection{Attributes} @defproc[(webview-set-attr! [wv wv-win?] @@ -556,10 +411,11 @@ Clears one or more inline style properties from the selected elements. [attr-entries (or/c kv? list-of-kv?)]) hash?]{ -Sets one or more HTML attributes on the selected elements. +Sets attributes using JavaScript. -Date, time, datetime, and color values are converted to strings before being -sent to the browser. +Dates, times, datetimes, and colors are converted to strings before being passed +to JavaScript. The effect is applied to all elements matched by +@racket[selector]. } @defproc[(webview-attr @@ -568,9 +424,9 @@ sent to the browser. [attr (or/c symbol? string?)]) (or/c string? boolean?)]{ -Returns the value of the named attribute of the element with the given id. +Returns the attribute value of the element identified by @racket[id]. -If the attribute does not exist, the result is @racket[#f]. +If the JavaScript result is @tt{null}, the function returns @racket[#f]. } @defproc[(webview-attr/boolean @@ -578,7 +434,8 @@ If the attribute does not exist, the result is @racket[#f]. [id symbol?] [attr (or/c symbol? string?)]) (or/c boolean? #f)]{ -Returns the attribute converted to a boolean. + +Returns the attribute value converted to a boolean. } @defproc[(webview-attr/symbol @@ -586,7 +443,8 @@ Returns the attribute converted to a boolean. [id symbol?] [attr (or/c symbol? string?)]) (or/c symbol? #f)]{ -Returns the attribute converted to a symbol. + +Returns the attribute value converted to a symbol. } @defproc[(webview-attr/number @@ -594,7 +452,8 @@ Returns the attribute converted to a symbol. [id symbol?] [attr (or/c symbol? string?)]) (or/c number? #f)]{ -Returns the attribute converted to a number. + +Returns the attribute value converted to a number. } @defproc[(webview-attr/date @@ -602,7 +461,8 @@ Returns the attribute converted to a number. [id symbol?] [attr (or/c symbol? string?)]) (or/c g:date? #f)]{ -Returns the attribute converted to a date. + +Returns the attribute value converted to a date. } @defproc[(webview-attr/time @@ -610,7 +470,8 @@ Returns the attribute converted to a date. [id symbol?] [attr (or/c symbol? string?)]) (or/c g:time? #f)]{ -Returns the attribute converted to a time. + +Returns the attribute value converted to a time. } @defproc[(webview-attr/datetime @@ -618,7 +479,8 @@ Returns the attribute converted to a time. [id symbol?] [attr (or/c symbol? string?)]) (or/c g:datetime? #f)]{ -Returns the attribute converted to a datetime. + +Returns the attribute value converted to a datetime. } @defproc[(webview-attr/color @@ -626,172 +488,69 @@ Returns the attribute converted to a datetime. [id symbol?] [attr (or/c symbol? string?)]) (or/c rgba? #f)]{ -Returns the attribute converted to an RGBA color. + +Returns the attribute value converted to a color. } -@section{Utility Functions} +@subsection{Inner HTML} + +@defproc[(webview-set-innerHTML! + [wv wv-win?] + [id symbol?] + [html (or/c string? xexpr?)]) + symbol?]{ + +Sets the innerHTML of the element identified by @racket[id] using JavaScript. + +Returns @racket['oke] if the injected JavaScript yields a true value, and +@racket['failed] otherwise. +} + +@section{File Filters} + +@defstruct*[wv-permitted-exts ([name string?] + [exts (listof symbol?)])]{ + +Represents a file dialog filter entry. +} + +@defproc[(make-wv-permitted-exts [name string?] [exts (listof symbol?)]) + wv-permitted-exts?]{Creates a filter entry.} + +@defproc[(wv-permitted-exts? [v any/c]) boolean?]{Recognizes filter entries.} + +@defproc[(wv-list-of-permitted-exts? [v any/c]) boolean?]{ +Recognizes lists of filter entries. +} + +@section{Utilities} + +@defproc[(webview-default-boilerplate-js [f procedure?] ...) + string?]{ +Returns the default JavaScript boilerplate. +} @defproc[(webview-standard-file-getter [base-path path-string?] - [#:not-exist on-not-exist procedure? - (λ (file base-path path) path)]) + [#:not-exist f procedure?]) 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. +Creates a standard file getter. } -@defproc[(webview-default-boilerplate-js [custom-js procedure?] ...) - string?]{ +@section{Diagnostics} -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) list?]{ +Returns version information. } -@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]. +@defproc[(webview-info) list?]{ +Returns runtime information. } -@section{Event Reference} +@defproc[(webview-set-loglevel [l (or/c 'error 'warning 'info 'debug)]) + void?]{ -Events are delivered to @racket[webview-create]'s callback as decoded Racket -hash tables. +Sets the native log level. -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} -] \ No newline at end of file +The native log file path can be obtained via @racket[webview-info]. +} \ No newline at end of file diff --git a/scrbl/rktwebview-api.scrbl b/scrbl/rktwebview-api.scrbl index d8dd45f..3eeb62f 100644 --- a/scrbl/rktwebview-api.scrbl +++ b/scrbl/rktwebview-api.scrbl @@ -1,42 +1,82 @@ #lang scribble/manual -@title{API Reference for @tt{rktwebview_qt}} +@;@defmodule[racket-webview] + @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] -@italic{For use with Racket FFI} -@section{Overview} +@title{C API for Racket Integration} -@tt{rktwebview_qt} exposes a small C-compatible API for creating and controlling -Qt-based webviews from Racket through the FFI. +This section describes the C API exactly as defined in @tt{rktwebview.h} and +@tt{rktwebview_types.h}. The API is used from Racket through FFI, but is +documented here in its native C/C++ form. -The public interface is declared in @tt{rktwebview.h}. Its implementation in -@tt{rktwebview.cpp} mainly forwards requests to the internal Qt runtime. - -The API consists of initialization and event processing, version and memory-management utility -functions, http(s)-context management, window management, navigation and content loading, -JavaScript execution and native dialogs. - -@section{Basic Types} - -@subsection{Window and Context Handles} - -The API uses integer handles for windows and contexts.A value of type @tt{rktwebview_t} identifies -a single webview window and of type @tt{rkt_wv_context_t} identifies a http(s) context. Both -are typedefs of @tt{int}. - -@subsection{Returned data, data for events and resultcodes} - -The following type is used for communication of data, version information and events. -All possible data that is communicated is assembled in this one type that will be -allocated by the API and must be released by the API user, using a call to +The interface is deliberately small. Handles are integers, most operations +return a @tt{result_t}, and structured values are returned as @tt{rkt_data_t *}. +The latter are caller-owned and must be released with @tt{rkt_webview_free_data()}. -@verbatim|{ -typedef enum { - version = 1, - event = 2, - js_result = 3 -} rkt_data_kind_t; +@section{Version, Export, and Basic Types} + +The public API version is: + +@verbatim{ +#define RKT_WEBVIEW_API_MAJOR 0 +#define RKT_WEBVIEW_API_MINOR 1 +#define RKT_WEBVIEW_API_PATCH 1 +} + +The export macro @tt{RKTWEBVIEW_EXPORT} expands to +@tt{__declspec(dllexport)} or @tt{__declspec(dllimport)} on Windows, and to an +empty definition on other platforms. + +The two basic handle types are: + +@verbatim{ +typedef int rktwebview_t; +typedef int rkt_wv_context_t; +} + +@tt{rktwebview_t} identifies a webview. @tt{rkt_wv_context_t} identifies a +context. + +@section{Enums and Structured Data} + +The API defines the following enums: @tt{rkt_webview_loglevel_t}, +@tt{result_t}, @tt{window_state_t}, @tt{rkt_messagetype_t}, and +@tt{rkt_data_kind_t}. The corresponding values are exactly those given in +@tt{rktwebview_types.h}. + +Structured return data uses these types: + +@verbatim{ +typedef struct { + rktwebview_t wv; + char *event; +} rkt_event_t; + +typedef struct { + result_t result; + char *value; +} rkt_js_result_t; + +typedef struct { + int api_major; + int api_minor; + int api_patch; +} rkt_version_t; + +typedef struct { + int shm_usage; + int shm_free_depth; + int shm_free_size; + int shm_item_depth; + int shm_item_size; + double shm_item_usage_factor; + int open_windows; + int function_calls; + int events; + char *log_file; +} rkt_metrics_t; typedef struct { rkt_data_kind_t kind; @@ -44,664 +84,505 @@ typedef struct { rkt_version_t version; rkt_event_t event; rkt_js_result_t js_result; + rkt_metrics_t metrics; } data; } rkt_data_t; -}| +} -The @tt{kind} field determines which member of the union is valid. A version field -is a struct of integer numbers: +The @tt{kind} field determines which member of @tt{data} is valid. -@verbatim|{ -typedef struct { - int qt_major; - int qt_minor; - int qt_patch; - int api_major; - int api_minor; - int api_patch; -} rkt_version_t; -}| +@section{Ownership} -Events are always for webview windows and consist of the webview handle along with -an event. This event is always a Utf-8 JSON string. In this JSON string, the @tt{"event"}} -item holds the name of the event that is triggered. +Any function returning @tt{rkt_data_t *} returns allocated memory. The caller +must release it using: -@verbatim|{ -typedef struct { - rktwebview_t wv; - char *event; -} rkt_event_t; -}| +@verbatim{ +RKTWEBVIEW_EXPORT void rkt_webview_free_data(rkt_data_t *d); +} -Returned data (mostly as a result of Javascript calls) consists of a result code and -a JSON encoded value. See following struct. +This applies to values returned by @tt{rkt_webview_info()}, +@tt{rkt_webview_version()}, @tt{rkt_webview_get_event()}, and +@tt{rkt_webview_call_js()}. -@verbatim|{ -typedef struct { - result_t result; - char *value; -} rkt_js_result_t; -}| +@section{Environment and Runtime Control} -Many functions return a value of type @tt{result_t}. The possible enumeration values -can be found in @tt{rktwebview.h}. Most important are the values @tt{no_result_yet}, which -is for internal use and equals @tt{-1}; @tt{oke = 0}, which tells that everything went alright, -and all values @tt{>0} indicate some kind of failure. +@defproc[(rkt_webview_env [env_cmds any/c]) void?]{ +C signature: -@subsection{Window state} +@verbatim{ +RKTWEBVIEW_EXPORT void rkt_webview_env(const char *env_cmds[]); +} -Window state is represented by @tt{window_state_t}. The following values are used: -@tt{invalid = -1}, represents an invalid window state, @tt{normal = 0x00} and @tt{normal_active = 0x10}, -window shown in normal state (0x00). All windows that are active, i.e. have focus for user input, will -have bit 5 set to 1. Other states: @tt{minimized = 0x01}, @tt{maximized = 0x02} or @tt{0x12}, -@tt{hidden = 0x03}. +Sets environment variables before the backend is initialized. The argument is a +null-terminated array of @tt{const char *}, each string typically having the +form @tt{"NAME=value"}. -@subsection{Messagebox types} +Return value: none. +} -Message boxes use the type @tt{rkt_messagetype_t} to indicate the type of message to present: -@tt{info = 1} for information, @tt{error = 2} for error messages, @tt{warning = 3} for warnings, -@tt{yes_no = 4} for questions that can be answered by 'yes' or 'no' and @tt{oke_cancel} for messages -that can be answered by 'oke' or 'cancel'. +@defproc[(rkt_webview_init) void?]{ +C signature: -@subsection{Callback functions for events} +@verbatim{ +RKTWEBVIEW_EXPORT void rkt_webview_init(); +} -A window is created with an event callback of the following type: +Initializes the backend, creates the shared memory transport, and starts the +helper process. -@verbatim|{ -typedef void (*event_cb_t)(rkt_data_t *); -}| +Return value: none. +} -The callback receives a pointer to a @tt{rkt_data_t} whose @tt{kind} is @tt{event}. The callback -argument must eventually be released by calling @tt{rkt_webview_free_data}. +@defproc[(rkt_webview_cleanup) void?]{ +C signature: -@subsection{Memory management} +@verbatim{ +RKTWEBVIEW_EXPORT void rkt_webview_cleanup(); +} -The following functions return heap-allocated @tt{rkt_data_t *} values: +Stops the backend and releases its shared resources. After this call, all +previously returned context and webview handles are invalid. -@itemlist[#:style 'compact - @item{@tt{rkt_webview_version}} - @item{@tt{rkt_webview_call_js}} - @item{the event callback receives event values allocated by the native side} -] +Return value: none. +} -Such values must be released by calling: +@defproc[(rkt_webview_set_loglevel [l any/c]) void?]{ +C signature: -@verbatim|{ -void rkt_webview_free_data(rkt_data_t *d); -}| +@verbatim{ +RKTWEBVIEW_EXPORT void rkt_webview_set_loglevel(rkt_webview_loglevel_t l); +} -This function frees both the outer structure and any string data owned by it. +Sets the backend log level. -@section{Initialization and Event Processing} +Return value: none. +} -@subsection{@tt{rkt_webview_init}} +@defproc[(rkt_webview_info) any/c]{ +C signature: -@verbatim|{ -void rkt_webview_init(); -}| +@verbatim{ +RKTWEBVIEW_EXPORT rkt_data_t *rkt_webview_info(); +} -Initializes the global runtime if it has not already been initialized. +Returns a pointer to @tt{rkt_data_t}. The returned object has +@tt{kind == metrics}; the valid payload is therefore @tt{data.metrics}. -This function is idempotent. The implementation checks whether the global -runtime already exists before constructing it. +Return value: @tt{rkt_data_t *}, caller-owned. +} -Most public API functions call @tt{rkt_webview_init} automatically before doing -their work, so explicit initialization is optional but harmless. +@defproc[(rkt_webview_version) any/c]{ +C signature: -@subsection{@tt{rkt_webview_process_events}} +@verbatim{ +RKTWEBVIEW_EXPORT rkt_data_t *rkt_webview_version(); +} -@verbatim|{ -void rkt_webview_process_events(int for_ms); -}| +Returns a pointer to @tt{rkt_data_t}. The returned object has +@tt{kind == version}; the valid payload is therefore @tt{data.version}. -Processes Qt events for approximately @tt{for_ms} milliseconds. +Return value: @tt{rkt_data_t *}, caller-owned. +} -The implementation repeatedly sleeps briefly and then calls the internal Qt -event-processing routine. This function is important for keeping windows -responsive and for delivering callbacks such as: +@subsection{Events} -@itemlist[#:style 'compact - @item{window lifecycle events} - @item{page-load events} - @item{JavaScript-originated events} - @item{dialog completion events} -] +@defproc[(rkt_webview_events_waiting) exact-integer?]{ +C signature: -Applications embedding this backend are expected to call this function -regularly. +@verbatim{ +RKTWEBVIEW_EXPORT int rkt_webview_events_waiting(); +} -@section{Version and Utility Functions} +Returns the number of events currently waiting in the event queue. -@subsection{@tt{rkt_webview_version}} +Return value: @tt{int}. +} -@verbatim|{ -rkt_data_t *rkt_webview_version(); -}| +@defproc[(rkt_webview_get_event) any/c]{ +C signature: -Returns a newly allocated @tt{rkt_data_t} whose @tt{kind} is @tt{version}. +@verbatim{ +RKTWEBVIEW_EXPORT rkt_data_t *rkt_webview_get_event(); +} -The returned structure contains: +Returns the next pending event. The returned object has @tt{kind == event}; the +valid payload is therefore @tt{data.event}. If no event is available, the +returned pointer may be null. -@itemlist[#:style 'compact - @item{the Qt version used to build the backend} - @item{the API version declared by the backend} -] +Return value: @tt{rkt_data_t *}, caller-owned, or null. +} -The returned value must be released with @tt{rkt_webview_free_data}. +@subsubsection{Event Polling} -@subsection{@tt{rkt_webview_free_data}} +Events are retrieved explicitly by polling. In normal use, polling should be +performed regularly; a polling interval of about 10 ms is appropriate. -@verbatim|{ -void rkt_webview_free_data(rkt_data_t *d); -}| +This keeps the event queue flowing, allows asynchronous operations such as +dialogs to complete in a timely manner, and avoids the impression that the system +has stalled while the Racket side is simply not looking. -Frees a @tt{rkt_data_t} that was allocated by the backend. +@section{Contexts} -The function inspects @tt{d->kind} and frees any owned string data before -freeing the outer object. +@defproc[(rkt_webview_new_context [boilerplate_js string?] + [optional_server_cert_pem string?]) + exact-integer?]{ +C signature: -@subsection{@tt{rkt_webview_set_ou_token}} +@verbatim{ +RKTWEBVIEW_EXPORT rkt_wv_context_t rkt_webview_new_context(const char *boilerplate_js, const char *optional_server_cert_pem); +} -@verbatim|{ -void rkt_webview_set_ou_token(rktwebview_t wv, const char *token); -}| +Creates a new context. A context corresponds to a @tt{QWebEngineProfile}; it +defines injected JavaScript and optional trust configuration using a trusted +self-signed certificate. -Associates an Organizational Unit token with the window. +Return value: @tt{rkt_wv_context_t}. +} -During certificate-error handling, self-signed certificates whose -Organizational Unit matches this token may be accepted automatically. +@section{Webviews} -This function does not return a status code. +@defproc[(rkt_webview_create [context exact-integer?] + [parent exact-integer?]) + exact-integer?]{ +C signature: -@subsection{Developer Tools} +@verbatim{ +RKTWEBVIEW_EXPORT int rkt_webview_create(rkt_wv_context_t context, rktwebview_t parent); +} -@subsubsection{@tt{rkt_webview_open_devtools}} +Creates a new webview in the given context. If @tt{parent} refers to an +existing webview, the new webview is created as a modal child of that parent; +otherwise it is created as a top-level window. -@verbatim|{ -result_t rkt_webview_open_devtools(rktwebview_t wv); -}| +Return value: webview handle as @tt{int}. +} -Opens a DevTools window associated with the given webview. +@defproc[(rkt_webview_close [wv exact-integer?]) void?]{ +C signature: -In the current implementation this function returns: +@verbatim{ +RKTWEBVIEW_EXPORT void rkt_webview_close(rktwebview_t wv); +} -@itemlist[#:style 'compact - @item{@tt{oke} on success} - @item{@tt{eval_js_failed} on failure} -] +Closes the specified webview. -Even though the @tt{result_t} enumeration contains -@tt{no_devtools_on_platform}, the implementation currently maps failure to -@tt{eval_js_failed}. +Return value: none. +} +@defproc[(rkt_webview_valid [wv exact-integer?]) boolean?]{ +C signature: -@section{HTTP(s) Contexts} +@verbatim{ +RKTWEBVIEW_EXPORT bool rkt_webview_valid(rktwebview_t wv); +} -@subsection{@tt{rkt_webview_new_context}} +Checks whether @tt{wv} is a valid webview handle. -@verbatim|{ -rkt_wv_context_t rkt_webview_new_context(const char *boilerplate_js, - const char *optional_server_cert_pem); -}| +Return value: @tt{bool}. +} -Creates a new http(s) context and returns its context handle. +@defproc[(rkt_webview_set_title [wv exact-integer?] [title string?]) + exact-integer?]{ +C signature: -A context corresponds to a Qt WebEngine profile and may be shared by multiple -windows. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_set_title(rktwebview_t wv, const char *title); +} -Arguments: +Sets the window title of the specified webview. -@itemlist[#:style 'compact - @item{@tt{boilerplate_js}: JavaScript source code to inject into pages} - @item{@tt{optional_server_cert_pem}: optional PEM-encoded certificate to trust} -] +Return value: @tt{result_t}. +} -Behavior: +@defproc[(rkt_webview_set_ou_token [wv exact-integer?] [token string?]) void?]{ +C signature: -@itemlist[#:style 'compact - @item{A fresh profile is created.} - @item{The backend injects built-in event-bridge JavaScript into the profile.} - @item{The supplied @tt{boilerplate_js} is also injected at document-ready time.} - @item{If a certificate string is supplied, it is added as an additional - trusted certificate.} -] +@verbatim{ +RKTWEBVIEW_EXPORT void rkt_webview_set_ou_token(rktwebview_t wv, const char *token); +} -The returned context handle can be passed to @tt{rkt_webview_create}. +Sets the organizational-unit token used during certificate validation for the +specified webview. -@section{Window Creation and Lifecycle} +Return value: none. +} -@subsection{@tt{rkt_webview_create}} +@section{Navigation and JavaScript} -@verbatim|{ -int rkt_webview_create(rkt_wv_context_t context, - rktwebview_t parent, - event_cb_t js_event_cb); -}| +@defproc[(rkt_webview_set_url [wv exact-integer?] [url string?]) exact-integer?]{ +C signature: -Creates a new window in the given context. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_set_url(rktwebview_t wv, const char *url); +} -Arguments: +Navigates the specified webview to the given URL. -@itemlist[#:style 'compact - @item{@tt{context}: the context handle returned by - @tt{rkt_webview_new_context}} - @item{@tt{parent}: a parent window handle, or a 0 value when no parent - is intended} - @item{@tt{js_event_cb}: callback used for all events emitted by this window} -] +Return value: @tt{result_t}. +} -Returns a window handle on success. +@defproc[(rkt_webview_set_html [wv exact-integer?] [html string?]) exact-integer?]{ +C signature: -The window is shown on success. When the parent window exists, the window -will be a modal window to the parent. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_set_html(rktwebview_t wv, const char *html); +} -The callback receives event notifications for that window, including page -events, geometry changes, JavaScript-originated events, and dialog results. +Loads raw HTML into the specified webview. -@subsection{@tt{rkt_webview_close}} +Return value: @tt{result_t}. +} -@verbatim|{ -void rkt_webview_close(rktwebview_t wv); -}| +@defproc[(rkt_webview_run_js [wv exact-integer?] [js string?]) exact-integer?]{ +C signature: -Requests the window to close. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_run_js(rktwebview_t wv, const char *js); +} -The implementation closes the window through the internal Qt command path. +Executes JavaScript asynchronously in the specified webview. -When a close completes, a @tt{"closed"} event is emitted to the callback. -This should also be called in response to a 'can-close? event. +Return value: @tt{result_t}. +} -@subsection{@tt{rkt_webview_valid}} +@defproc[(rkt_webview_call_js [wv exact-integer?] [js string?]) any/c]{ +C signature: -@verbatim|{ -bool rkt_webview_valid(rktwebview_t wv); -}| +@verbatim{ +RKTWEBVIEW_EXPORT rkt_data_t *rkt_webview_call_js(rktwebview_t wv, const char *js); +} -Returns @tt{#t} when the given window handle currently exists and is known to -the runtime, and @tt{#f} otherwise. +Executes JavaScript synchronously in the specified webview. -@section{Window Configuration} +The provided JavaScript must end with a @tt{return} statement. The returned value +must be convertible to JSON using @tt{JSON.stringify}, as the result is +serialized and returned as a JSON string. -@subsection{@tt{rkt_webview_set_title}} +The returned object has @tt{kind == js_result}; the valid payload is therefore +@tt{data.js_result}. The field @tt{data.js_result.value} contains the JSON +result string. -@verbatim|{ -result_t rkt_webview_set_title(rktwebview_t wv, const char *title); -}| +Return value: @tt{rkt_data_t *}, caller-owned. +} -Sets the native title of the window. +@defproc[(rkt_webview_open_devtools [wv exact-integer?]) exact-integer?]{ +C signature: -Returns @tt{oke} on success and @tt{failed} on failure. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_open_devtools(rktwebview_t wv); +} +Opens developer tools for the specified webview. -@section{Navigation and Content Loading} +Return value: @tt{result_t}. +} -@subsection{@tt{rkt_webview_set_url}} +@section{Window Management} -@verbatim|{ -result_t rkt_webview_set_url(rktwebview_t wv, const char *url); -}| +@defproc[(rkt_webview_move [w exact-integer?] [x exact-integer?] [y exact-integer?]) + exact-integer?]{ +C signature: -Navigates the window to the given URL. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_move(rktwebview_t w, int x, int y); +} -Returns: +Moves the specified window to the given screen coordinates. -@itemlist[#:style 'compact - @item{@tt{oke} on success} - @item{@tt{set_navigate_failed} on failure} -] +Return value: @tt{result_t}. +} -A successful or failed load eventually also produces a @tt{"page-loaded"} event. +@defproc[(rkt_webview_resize [w exact-integer?] + [width exact-integer?] + [height exact-integer?]) + exact-integer?]{ +C signature: -@subsection{@tt{rkt_webview_set_html}} +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_resize(rktwebview_t w, int width, int height); +} -@verbatim|{ -result_t rkt_webview_set_html(rktwebview_t wv, const char *html); -}| +Resizes the specified window. -Loads the given HTML string directly into the window. +Return value: @tt{result_t}. +} -Returns: +@defproc[(rkt_webview_hide [w exact-integer?]) exact-integer?]{ +C signature: -@itemlist[#:style 'compact - @item{@tt{oke} on success} - @item{@tt{set_html_failed} on failure} -] +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_hide(rktwebview_t w); +} -A successful or failed load eventually also produces a @tt{"page-loaded"} event. +Hides the specified window. -@section{JavaScript Execution} +Return value: @tt{result_t}. +} -@subsection{@tt{rkt_webview_run_js}} +@defproc[(rkt_webview_show [w exact-integer?]) exact-integer?]{ +C signature: -@verbatim|{ -result_t rkt_webview_run_js(rktwebview_t wv, const char *js); -}| +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_show(rktwebview_t w); +} -Executes JavaScript in the page without returning a value to the caller. +Shows the specified window. -Returns: +Return value: @tt{result_t}. +} -@itemlist[#:style 'compact - @item{@tt{oke} on success} - @item{@tt{eval_js_failed} on failure} -] +@defproc[(rkt_webview_show_normal [w exact-integer?]) exact-integer?]{ +C signature: -This function is suitable for side-effecting JavaScript. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_show_normal(rktwebview_t w); +} -@subsection{@tt{rkt_webview_call_js}} +Restores the specified window to its normal state. -@verbatim|{ -rkt_data_t *rkt_webview_call_js(rktwebview_t wv, const char *js); -}| +Return value: @tt{result_t}. +} -Executes JavaScript and returns a newly allocated @tt{rkt_data_t} whose -@tt{kind} is @tt{js_result}. +@defproc[(rkt_webview_present [w exact-integer?]) exact-integer?]{ +C signature: -The returned @tt{js_result.value} field contains a compact JSON string. +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_present(rktwebview_t w); +} -The implementation wraps the supplied code in a small JavaScript function and -returns a JSON object of the form: +Presents the specified window to the user. -@verbatim|{ -{"oke": true|false, "result": ..., "exn": ...} -}| +Return value: @tt{result_t}. +} +@defproc[(rkt_webview_maximize [w exact-integer?]) exact-integer?]{ +C signature: -The outer native result code is stored in @tt{js_result.result}: +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_maximize(rktwebview_t w); +} -@itemlist[#:style 'compact - @item{@tt{oke} when the wrapped JavaScript completed successfully, @tt{result} will contain the javascript result. } - @item{@tt{eval_js_failed} when the wrapped JavaScript reported failure, @tt{exn} will contain the javascript exception information.} -] +Maximizes the specified window. -The returned value must be released with @tt{rkt_webview_free_data}. +Return value: @tt{result_t}. +} -The javascript code is executed as part of an anonymous function and must always end with a @tt{return} statement to return the result. +@defproc[(rkt_webview_minimize [w exact-integer?]) exact-integer?]{ +C signature: +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_minimize(rktwebview_t w); +} -@section{Window Geometry and Visibility} +Minimizes the specified window. -@subsection{@tt{rkt_webview_move}} +Return value: @tt{result_t}. +} -@verbatim|{ -result_t rkt_webview_move(rktwebview_t wv, int x, int y); -}| +@defproc[(rkt_webview_window_state [w exact-integer?]) exact-integer?]{ +C signature: -Moves the window to the given screen position. +@verbatim{ +RKTWEBVIEW_EXPORT window_state_t rkt_webview_window_state(rktwebview_t w); +} -Returns: +Returns the current state of the specified window. -@itemlist[#:style 'compact - @item{@tt{oke} on success} - @item{@tt{move_failed} on failure} -] +Return value: @tt{window_state_t}. +} -@subsection{@tt{rkt_webview_resize}} +@section{Dialogs} -@verbatim|{ -result_t rkt_webview_resize(rktwebview_t wv, int width, int height); -}| +The dialog functions are asynchronous. They request that the dialog be opened on +the Qt side, but do not block the Racket side while the dialog is shown. This is +intentional: the Qt event loop and the Racket runtime should not stall each +other. -Resizes the window to the given dimensions. +Completion of a dialog is therefore observed through events, not by blocking the +calling thread. -Returns: -@itemlist[#:style 'compact - @item{@tt{oke} on success} - @item{@tt{resize_failed} on failure} -] +@defproc[(rkt_webview_choose_dir [w exact-integer?] + [title string?] + [base_dir string?]) + exact-integer?]{ +C signature: -@subsection{@tt{rkt_webview_hide}} +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_choose_dir(rktwebview_t w, const char *title, const char *base_dir); +} -@verbatim|{ -result_t rkt_webview_hide(rktwebview_t w); -}| +Opens a directory chooser associated with the specified webview. +This call is asynchronous. The function returns immediately; dialog completion is +reported through events. -Hides the window. +Return value: @tt{result_t}. +} -Returns @tt{oke} on success and @tt{failed} on failure. +@defproc[(rkt_webview_file_open [w exact-integer?] + [title string?] + [base_dir string?] + [permitted_exts string?]) + exact-integer?]{ +C signature: -@subsection{@tt{rkt_webview_show}} +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_file_open(rktwebview_t w, const char *title, const char *base_dir, const char *permitted_exts); +} -@verbatim|{ -result_t rkt_webview_show(rktwebview_t w); -}| +Opens a file-open dialog associated with the specified webview. +This call is asynchronous. The function returns immediately; dialog completion is +reported through events. -Shows the window. +Return value: @tt{result_t}. +} -Returns @tt{oke} on success and @tt{failed} on failure. +@defproc[(rkt_webview_file_save [w exact-integer?] + [title string?] + [base_dir string?] + [permitted_exts string?]) + exact-integer?]{ +C signature: -@subsection{@tt{rkt_webview_show_normal}} +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_file_save(rktwebview_t w, const char *title, const char *base_dir, const char *permitted_exts); +} -@verbatim|{ -result_t rkt_webview_show_normal(rktwebview_t w); -}| +Opens a file-save dialog associated with the specified webview. +This call is asynchronous. The function returns immediately; dialog completion is +reported through events. -Restores the window to the normal state. +Return value: @tt{result_t}. +} -Returns @tt{oke} on success and @tt{failed} on failure. +@defproc[(rkt_webview_message_box [w exact-integer?] + [title string?] + [message string?] + [submessage string?] + [type exact-integer?]) + exact-integer?]{ +C signature: -@subsection{@tt{rkt_webview_present}} +@verbatim{ +RKTWEBVIEW_EXPORT result_t rkt_webview_message_box(rktwebview_t w, const char *title, const char *message, const char *submessage, rkt_messagetype_t type); +} -@verbatim|{ -result_t rkt_webview_present(rktwebview_t w); -}| +Opens a message box of the given type, associated with the specified webview. +This call is asynchronous. The function returns immediately; dialog completion is +reported through events. -Requests that the window be presented to the user. +Return value: @tt{result_t}. +} -Returns @tt{oke} on success and @tt{failed} on failure. +@section{Remarks} -@subsection{@tt{rkt_webview_maximize}} +The API is queue-based. Commands are issued through function calls; events are +retrieved explicitly using @tt{rkt_webview_events_waiting()} and +@tt{rkt_webview_get_event()}. The boundary stays clear: integer handles on the +outside, Qt objects on the inside. -@verbatim|{ -result_t rkt_webview_maximize(rktwebview_t w); -}| - -Maximizes the window. - -Returns @tt{oke} on success and @tt{failed} on failure. - -@subsection{@tt{rkt_webview_minimize}} - -@verbatim|{ -result_t rkt_webview_minimize(rktwebview_t w); -}| - -Minimizes the window. - -Returns @tt{oke} on success and @tt{failed} on failure. - -@subsection{@tt{rkt_webview_window_state}} - -@verbatim|{ -window_state_t rkt_webview_window_state(rktwebview_t w); -}| - -Returns the current state of the window. - -Possible results include: - -@itemlist[#:style 'compact - @item{@tt{normal}} - @item{@tt{minimized}} - @item{@tt{maximized}} - @item{@tt{hidden}} - @item{@tt{normal_active}} - @item{@tt{maximized_active}} - @item{@tt{invalid} when no valid state can be reported} -] - -@section{Native Dialogs} - -The dialog functions display native Qt dialogs and report the user's choice by -emitting events through the window callback. - -@subsection{@tt{rkt_webview_choose_dir}} - -@verbatim|{ -result_t rkt_webview_choose_dir(rktwebview_t w, - const char *title, - const char *base_dir); -}| - -Opens a directory-selection dialog. - -Returns: - -@itemlist[#:style 'compact - @item{@tt{oke} if the dialog was shown} - @item{@tt{failed} if the window handle is invalid} -] - -On completion the callback receives one of these events: - -@itemlist[#:style 'compact - @item{@tt{"choose-dir-ok"}} - @item{@tt{"choose-dir-cancel"}} -] - -For the @tt{"choose-dir-ok"} event, the JSON payload includes fields such as -the chosen path and current directory. - -@subsection{@tt{rkt_webview_file_open}} - -@verbatim|{ -result_t rkt_webview_file_open(rktwebview_t w, - const char *title, - const char *base_dir, - const char *permitted_exts); -}| - -Opens a file-open dialog. - -The @tt{permitted_exts} argument is passed to Qt as a filter string using the -usual @tt{";;"} separator format. - -Returns: - -@itemlist[#:style 'compact - @item{@tt{oke} if the dialog was shown} - @item{@tt{failed} if the window handle is invalid} -] - -On completion the callback receives one of these events: - -@itemlist[#:style 'compact - @item{@tt{"file-open-ok"}} - @item{@tt{"file-open-cancel"}} -] - -@subsection{@tt{rkt_webview_file_save}} - -@verbatim|{ -result_t rkt_webview_file_save(rktwebview_t w, - const char *title, - const char *base_dir, - const char *permitted_exts); -}| - -Opens a file-save dialog. - -The @tt{permitted_exts} argument is passed to Qt as a filter string using the -usual @tt{";;"} separator format. - -Returns: - -@itemlist[#:style 'compact - @item{@tt{oke} if the dialog was shown} - @item{@tt{failed} if the window handle is invalid} -] - -On completion the callback receives one of these events: - -@itemlist[#:style 'compact - @item{@tt{"file-save-ok"}} - @item{@tt{"file-save-cancel"}} -] - -@subsection{@tt{rkt_webview_message_box}} - -@verbatim|{ -result_t rkt_webview_message_box(rktwebview_t w, - const char *title, - const char *message, - const char *submessage, - rkt_messagetype_t type); -}| - -Shows a native message box. - -Arguments: - -@itemlist[#:style 'compact - @item{@tt{title}: window title of the message box} - @item{@tt{message}: main message} - @item{@tt{submessage}: supplementary message text} - @item{@tt{type}: one of @tt{info}, @tt{error}, @tt{warning}, - @tt{yes_no}, or @tt{oke_cancel}} -] - -Returns: - -@itemlist[#:style 'compact - @item{@tt{oke} if the dialog was shown} - @item{@tt{failed} if the window handle is invalid} -] - -The callback receives one of the following events, depending on the chosen -button: - -@itemlist[#:style 'compact - @item{@tt{"msgbox-ok"}} - @item{@tt{"msgbox-cancel"}} - @item{@tt{"msgbox-yes"}} - @item{@tt{"msgbox-no"}} -] - -@section{Event Model} - -The callback passed to @tt{rkt_webview_create} receives JSON-encoded events. - -The implementation emits events such as: - -@itemlist[#:style 'compact - @item{@tt{"show"}} - @item{@tt{"hide"}} - @item{@tt{"move"}} - @item{@tt{"resize"}} - @item{@tt{"closed"}} - @item{@tt{"can-close?"}} - @item{@tt{"page-loaded"}} - @item{@tt{"navigation-request"}} - @item{@tt{"link-hovered"}} - @item{@tt{"js-evt"}} - @item{dialog result events such as @tt{"choose-dir-ok"} and @tt{"msgbox-yes"}} -] - -The precise JSON structure depends on the event kind. Examples include: - -@itemlist[#:style 'compact - @item{@tt{"page-loaded"} with an @tt{"oke"} field} - @item{@tt{"move"} with @tt{"x"} and @tt{"y"}} - @item{@tt{"resize"} with @tt{"w"} and @tt{"h"}} - @item{@tt{"navigation-request"} with @tt{"url"} and @tt{"type"}} - @item{@tt{"link-hovered"} with @tt{"url"}} - @item{@tt{"js-evt"} with a nested @tt{"js-evt"} object containing the - original JavaScript payload} -] - -@section{Typical Usage Pattern} - -A typical embedding sequence is: - -@itemlist[#:style 'compact - @item{Create a context with @tt{rkt_webview_new_context}.} - @item{Create a window with @tt{rkt_webview_create}.} - @item{Load content with @tt{rkt_webview_set_url} or @tt{rkt_webview_set_html}.} - @item{Interact with the page using @tt{rkt_webview_run_js} or - @tt{rkt_webview_call_js}.} - @item{Call @tt{rkt_webview_process_events} regularly to keep the UI alive.} - @item{Release all returned @tt{rkt_data_t *} values with - @tt{rkt_webview_free_data}.} -] +Dialog functions are asynchronous and do not block the Racket side while a +dialog is shown. Their completion is observed through events. +In normal use, event polling should be performed regularly. A polling interval +of about 10 ms is appropriate. \ No newline at end of file diff --git a/scrbl/rktwebview-shared-memory-diagram-simple.svg b/scrbl/rktwebview-shared-memory-diagram-simple.svg new file mode 100644 index 0000000..b03e9bc --- /dev/null +++ b/scrbl/rktwebview-shared-memory-diagram-simple.svg @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Shared Memory + + + + + + + + Allocator + + free list, slot table, allocation headers + + + + Command Queue + command payloads + + + Result Queue + result payloads + + + Event Queue + event payloads + + Payloads in Shared Memory + + + + + + Racket Process + + • Call API (FFI) + • Receive Result + • Poll Events + + + + + rktwebview_prg + (Qt / Chromium) + + + Command Thread + + + + + Qt GUI Thread + + + + + + + + Racket calls the C API through FFI; the helper process consumes commands and produces results and events. + + + + + + + diff --git a/scrbl/rktwebviewqt-internals.scrbl b/scrbl/rktwebviewqt-internals.scrbl index 1fff21a..c5e7edb 100644 --- a/scrbl/rktwebviewqt-internals.scrbl +++ b/scrbl/rktwebviewqt-internals.scrbl @@ -1,346 +1,256 @@ #lang scribble/manual -@title{Structure and behavior of @tt{rktwebview_qt}} +@title{Qt WebView Backend Architecture} @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} -@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. +This backend provides a webview implementation by delegating all GUI and browser +functionality to a separate Qt process. -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 - @item{creation of browser windows} - @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} -] +This design exists to work around limitations of Qt WebEngine in combination with +the lifecycle model of the DrRacket environment. -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. +@section{Execution Model} -@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 -components provided by Qt. Earlier Qt versions are not supported. +@section{Shared Memory and Queues} -Applications using the backend must therefore ensure that a compatible -Qt runtime (≥ @tt{6.10.2}) is available. +A shared memory region is created during initialization. Inside that region, +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 -with the backend library. +Each message consists of a numeric code and a payload, typically JSON: -@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 -@tt{setAdditionalTrustedCertificates} provided by -@tt{QWebEngineProfileBuilder}. This functionality allows the backend to -install additional trusted certificates for a context. +@section{Command Execution} -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. +A function call on the embedding side is translated into a command and written to +the command queue. -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 caller’s 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 -C-compatible interface that can easily be consumed from Racket through FFI. +Many relevant events are not tied to a specific command. Page loading, navigation +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 - @item{numeric handles represent windows and browser contexts} - @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} -] +Each event contains a name, an identifier, and optional fields depending on its +type. Events are delivered in FIFO order. -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{Contexts and Webviews} -@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 - @item{owning the Qt application instance} - @item{managing https contexts} - @item{managing open windows} - @item{dispatching commands} - @item{forwarding events to the host application} -] +Within a context, one or more webviews can be created. A webview represents a +window containing a browser view. Webviews are also identified by integer +handles. -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 - @item{a map from window handles to @tt{WebviewWindow} instances} - @item{a map from context identifiers to @tt{QWebEngineProfile} objects} - @item{a map from window handles to callback functions} -] +Webviews may optionally have a parent webview. If a parent is specified, the +resulting window is created as a modal child of that parent; otherwise it is +created as a top-level window. -Each API call that manipulates a window or browser state is forwarded through -this controller. +From the Racket side, this means that a context must be created first. That +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 - @item{a @tt{QWebEngineProfile}} - @item{optional injected JavaScript code} - @item{optional trusted certificates} -] +@centerline{@tt{window.rkt_send_event(obj)}} -Multiple windows normally share the same context. This allows cookies, -configuration, and injected scripts to be reused. +The objects are collected and forwarded to the event queue. -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 -top-level window. +In practice, once a @tt{QApplication} using WebEngine has been started and shut +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 - @item{hosting the browser widget} - @item{reporting window events such as show, hide, move, and resize} - @item{handling close requests} - @item{reporting navigation events} - @item{forwarding JavaScript events} -] +In the DrRacket environment, where components may be restarted under a +custodian, this makes an in-process design fundamentally unsuitable. Even when +libraries are loaded and unloaded using @tt{#:custodian}, the WebEngine runtime +cannot be reset to a clean state. -Window geometry changes are slightly delayed through timers so that the host -application receives fewer intermediate events. +By moving Qt and WebEngine into a separate process, this limitation is avoided +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 -the Qt widget with a numeric handle used by the external API. +Instead of attempting to integrate Qt’s event loop with Racket, the design +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 Qt’s 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 -Qt event system using a custom Qt event. The runtime controller receives the -event and executes the corresponding operation. +Running the Qt side in a separate process provides isolation: if the helper +process terminates, the embedding Racket process remains unaffected and can +decide how to recover. -This design ensures that GUI operations occur inside the Qt event loop while -the external API remains synchronous. +@bold{Shared memory communication.} +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 -@tt{rkt_webview_process_events}, which internally calls -@tt{QApplication::processEvents}. This allows the host runtime to control how -frequently GUI events are processed. +For control commands, payload sizes are small and infrequent, so serialization +cost is negligible compared to GUI-thread execution and WebEngine processing; for +dynamic data such as JavaScript results and custom events, JSON is the +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 - @item{@tt{rkt_webview_set_url} loads a URL} - @item{@tt{rkt_webview_set_html} loads raw HTML} -] +At the start of the shared memory block, a small administration area is stored, +including a pointer to the current end of allocated memory, a list of active +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 -contains a boolean field indicating whether loading succeeded. - -@section{JavaScript Execution} - -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 - @item{@tt{oke}} - @item{@tt{result}} - @item{@tt{exn}} -] - -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 - @item{@tt{window.rkt_event_queue}} - @item{@tt{window.rkt_send_event(obj)}} - @item{@tt{window.rkt_get_events()}} -] - -The queue stores JavaScript-generated events until the native side retrieves -them. - -@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. \ No newline at end of file +Memory allocation inside the shared block is intentionally simple. Each +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 +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 +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. + +Shared objects are referenced primarily through offsets (via @tt{ShmPlace}) +rather than raw pointers. This makes the layout independent of the virtual +address at which the shared memory is mapped in each process. + +Queues are built directly on top of this allocator. Each queue consists of a +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 +pointer to its payload in shared memory, and links to neighboring items. + +Synchronization is split into two layers. A shared lock protects allocator and +queue metadata, while each queue has its own semaphore indicating whether items +are available. One mechanism protects the structure; the other tells you whether +there is anything worth reading. + +On POSIX systems such as Linux, shared memory is implemented using +@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}. + +On Windows, the same model is implemented using @tt{CreateFileMappingA} and +@tt{MapViewOfFile} for shared memory, and @tt{CreateSemaphoreA} or +@tt{OpenSemaphoreA} for synchronization. The design is identical, but the kernel +objects follow the Windows lifetime model and are released when the last handle +is closed. + +The shared memory region has a fixed size (currently 10MB) and is not resized at +runtime. Although the use of @tt{ShmPlace} offsets would in principle allow +relocation, resizing would require coordinated remapping in both processes while +all activity is paused. The current design therefore treats the region as +fixed-size and relies on reuse of freed blocks. + +This implies that the communication channel is bounded. Payloads such as +@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 +messages. + +This is a message transport, not an infinite sack of HTML. diff --git a/scrbl/wv-context.scrbl b/scrbl/wv-context.scrbl index 205876c..c07e516 100644 --- a/scrbl/wv-context.scrbl +++ b/scrbl/wv-context.scrbl @@ -1,80 +1,107 @@ #lang scribble/manual -@(require scribble/racket - scribble/example +@(require racket/base + racket/class scribble/core (for-label racket/base + racket/string racket/class - racket/path - "../private/wv-settings.rkt" - "../private/racket-webview.rkt") - ) + racket/file)) -@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"]] +@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% ()]{ -An OO wrapper around a webview context together with its associated settings. -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%] -instance for access to global and per-section settings. This class manages -construction of a webview context using a file getter and boilerplate JavaScript, -exposure of the created low-level context object via @racket[context], -access to the base URL of the created context via @racket[base-url] and -access to settings views via @racket[settings]. +Represents a webview context. + +A @racket[wv-context%] object is a thin class-based wrapper around the +higher-level context support provided by @racketmodname[racket-webview]. It +creates and stores a @racket[wv-context] value internally and provides methods +to access that value, its base URL, and a settings object. @defconstructor[([base-path path-string?] - [file-getter procedure? (webview-standard-file-getter base-path)] - [context-js procedure? (λ () "")] - [boilerplate-js procedure? (webview-default-boilerplate-js context-js)] - [ini object?])]{ -Creates a new @racket[wv-context%] object. + [file-getter procedure? + (webview-standard-file-getter base-path)] + [context-js procedure? + (λ () "")] + [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 -@racket[file-getter]. +Creates a new context object. -The @racket[file-getter] argument is passed to @racket[webview-new-context] -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: +The constructor accepts the following initialization fields. @itemlist[#:style 'compact - @item{creates a new low-level context with @racket[webview-new-context];} - @item{creates a @racket[wv-settings%] object using @racket[ini] and the - global context identifier @racket['global].} + @item{@racket[base-path] is the base path used by the default file getter.} + @item{@racket[file-getter] is the procedure used to resolve files for the + 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?])]{ -Returns the underlying webview context (struct) created during -initialization. +@defmethod[(context) wv-context?]{ + +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%)])] { -Returns a @tt{wv-settings%} object for @racket[section]. -This method delegates to the internally stored @racket[wv-settings%] object by -calling @racket[(send settings-obj clone section)]. The resulting object shares -the same @racket[ini] backend as the global settings object, but addresses the -supplied section. +@defmethod[(settings [section symbol?]) wv-settings%]{ + +Returns a cloned settings object for @racket[section]. + +The method delegates to the internal @racket[wv-settings%] instance as: + +@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])] { -Returns the base URL of the underlying webview context. -This method delegates to @racket[wv-context-base-url]. -} +@defmethod[(base-url) string?]{ +Returns the base URL of the underlying context. + +The method delegates to @racket[wv-context-base-url] applied to the internal +context value. } +} \ No newline at end of file diff --git a/scrbl/wv-element.scrbl b/scrbl/wv-element.scrbl new file mode 100644 index 0000000..146d58a --- /dev/null +++ b/scrbl/wv-element.scrbl @@ -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) +] +} +} \ No newline at end of file diff --git a/scrbl/wv-input.scrbl b/scrbl/wv-input.scrbl new file mode 100644 index 0000000..7676ea2 --- /dev/null +++ b/scrbl/wv-input.scrbl @@ -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) +] +} +} \ No newline at end of file diff --git a/scrbl/wv-settings.scrbl b/scrbl/wv-settings.scrbl index a047baf..c61961a 100644 --- a/scrbl/wv-settings.scrbl +++ b/scrbl/wv-settings.scrbl @@ -1,69 +1,117 @@ #lang scribble/manual -@(require scribble/racket - scribble/example +@(require racket/base + racket/class scribble/core (for-label racket/base racket/string racket/class - racket/file) - ) + racket/file)) -@defmodule[racket-webview] - -@title{Object-Oriented Interface: @racket[wv-settings%]} +@title{wv-settings} @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% ()]{ -An OO wrapper around a settings backend implementing, e.g. @racket[simple-ini/class]. -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. +Wraps an @racket[ini] settings object for one context. -@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?])]{ -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)]} +Creates a settings wrapper. + +@racket[ini] must provide compatible @racket[get] and @racket[set!] methods. +@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] - [(get [key symbol?] [default-value any/c]) any/c])]{ -Returns the value associated with @racket[key] in the current section. -Returns @tt{default-value} (when supplied) if the key is not found. +@defmethod[(set! [key symbol?] [value any/c]) any/c]{ + +Stores @racket[value] under @racket[key] in the current context. + +Delegates to: + +@racketblock[ +(send ini set! wv-context key value) +] } -@defmethod*[([(set! [key symbol?] [value any/c]) this])]{ -Stores @racket[value] under @racket[key] in the current section. +@defmethod[(get/global [key symbol?] [default-value any/c] ...) any/c]{ + +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] - [(get/global [key symbol?] [default-value any/c]) any/c])]{ -Returns the value associated with @racket[key] in the global section. -This method delegates to @tt{(send ini get 'global key default-value)}. +@defmethod[(set/global! [key symbol?] [value any/c]) any/c]{ + +Stores @racket[value] under @racket[key] in the global settings section. + +Delegates to: + +@racketblock[ +(send ini set! 'global key value) +] } -@defmethod*[([(set/global! [key symbol?] [value any/c]) this])]{ -Stores @racket[value] under @racket[key] in the global section. -Delegates to @tt{(send ini set! 'global key value)}. +@defmethod[(clone [context symbol?]) wv-settings%]{ + +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%)])]{ -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?]{ -@defmethod*[([(context) symbol?])]{ -Returns the section identifier associated with this settings object. +Returns the current context symbol. } - } \ No newline at end of file diff --git a/scrbl/wv-window.scrbl b/scrbl/wv-window.scrbl index 76daadc..68f81eb 100644 --- a/scrbl/wv-window.scrbl +++ b/scrbl/wv-window.scrbl @@ -1,256 +1,457 @@ #lang scribble/manual -@(require scribble/racket - scribble/example +@(require racket/base + racket/class scribble/core (for-label racket/base + racket/string racket/class - racket/path + racket/file net/url "../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] -@title{Object-Oriented Interface: @racket[wv-window%]} -@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] +Window abstraction built on top of @racketmodname[racket-webview]. + +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% ()]{ -An OO wrapper around a single webview window. -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. +Represents one window in a webview context. -The class is designed so that subclasses can override event methods such as -@racket[moved], @racket[resized], @racket[page-loaded], @racket[js-event], -@racket[navigation-request], @racket[can-close?], and @racket[closed]. +The class wraps a lower-level window created through +@racketmodname[racket-webview] and connects it to the surrounding class-based +API. @defconstructor[([parent (or/c #f (is-a?/c wv-window%)) #f] - [wv-context (is-a?/c wv-context%) (if (eq? parent #f) - (error "wv context is mandatory") - (get-field wv-context parent))] - [html-path path-string? (error "html path is mandatory")] - [settings (is-a?/c wv-settings%) - (send wv-context settings + [wv-context (if (eq? parent #f) + (error "wv context is mandatory") + (get-field wv-context parent))] + [html-path path-string?] + [settings (send wv-context settings (string->symbol (format "~a" html-path)))] [title string? "Racket Webview Window"] - [width (or/c #f exact-integer?) #f] - [height (or/c #f exact-integer?) #f] - [x (or/c #f exact-integer?) #f] - [y (or/c #f exact-integer?) #f] - [wv any/c #f])]{ -Creates a new window. + [width (or/c exact-integer? #f) #f] + [height (or/c exact-integer? #f) #f] + [x (or/c exact-integer? #f) #f] + [y (or/c exact-integer? #f) #f])]{ -The @racket[wv-context] argument supplies the underlying webview context. -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. +Creates a window. -The constructor creates the low-level window using @racket[webview-create] and then -calls @racket[init-size] to restore position and size from settings, falling back to -constructor arguments or internal defaults. +The constructor requires @racket[html-path]. If @racket[parent] is @racket[#f], +@racket[wv-context] is mandatory. If a parent window is supplied, +@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%)])]{ -Returns the window context object associated with this window. +@defmethod[(context) any/c]{ + +Returns the window context object supplied at construction. } -@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. +@defmethod[(win-context) symbol?]{ + +Returns the settings context key for this window. + +The value is computed as: + +@racketblock[ +(string->symbol (format "~a" html-path)) +] } -@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], +@defmethod[(info) hash?]{ + +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]. } -@defmethod*[([(element [id symbol?]) any/c] - [(element [id symbol?] [type symbol?]) any/c])]{ -Returns the wrapper object for the DOM element with identifier @racket[id]. +@defmethod[(element [id symbol?] [type symbol?] ...) any/c]{ -If the element has not been wrapped before, a new wrapper object is created and cached. -When @racket[type] is omitted, the method attempts to determine the HTML input type via -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%], -@racket[wv-input/number%], or falls back to @racket[wv-element%]. +Returns the wrapper object associated with the DOM element identified by +@racket[id]. + +Element wrappers are cached in an internal weak hash table. If no wrapper exists +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?])]{ -Event hook called when the window has moved. -The default implementation stores the new position in the settings object. -Subclasses may override this method. +@defmethod[(moved [xx exact-integer?] [yy exact-integer?]) any/c]{ + +Updates the internal position and stores the new coordinates in +@racket[settings] under @racket['x] and @racket['y]. } -@defmethod*[([(resized [width exact-integer?] [height exact-integer?]) void?])]{ -Event hook called when the window has been resized. -The default implementation stores the new size in the settings object. -Subclasses may override this method. +@defmethod[(resized [w exact-integer?] [h exact-integer?]) any/c]{ + +Updates the internal size and stores the new dimensions in +@racket[settings] under @racket['width] and @racket['height]. } -@defmethod*[([(window-state-changed [state any/c]) 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[(window-state-changed [st symbol?]) any/c]{ -@defmethod*[([(page-loaded [ok? any/c]) any/c])]{ -Event hook called after the page load completes. The argument indicates whether the -load succeeded. The default implementation returns @racket[#t]. -} +Called when the underlying window state changes. -@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]. } -@defmethod*[([(closed) any/c])]{ -Event hook called after the window has been closed. +@defmethod[(page-loaded [oke any/c]) any/c]{ + +Called when a @racket['page-loaded] event is received. + The default implementation returns @racket[#t]. } -@defmethod*[([(js-event [js-event hash?]) any/c])]{ -Handles a JavaScript-originated event. +@defmethod[(can-close?) any/c]{ -The default implementation looks up the wrapped element referenced by the event and -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 -application-specific dispatch. +Called when a @racket['can-close?] event is received. + +If this method returns a true value, the window closes itself by calling +@racket[close]. + +The default implementation returns @racket[#t]. } -@defmethod*[([(navigation-request [type symbol?] [url url?]) (or/c 'internal 'external)])]{ -Handles a navigation request. +@defmethod[(closed) any/c]{ -If the requested URL starts with the context base URL, the window navigates internally -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]. +Called when a @racket['closed] event is received. + +The default implementation returns @racket[#t]. } -@defmethod*[([(add-class! [selector-or-id string?] [class string?]) this])]{ -Adds the CSS class @racket[class] to the elements selected by @racket[selector-or-id]. -Delegates to @racket[webview-add-class!]. +@defmethod[(js-event [js-event hash?]) any/c]{ + +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])]{ -Removes the CSS class @racket[class] from the elements selected by @racket[selector-or-id]. -Delegates to @racket[webview-remove-class!]. +@defmethod[(navigation-request [type symbol?] [url url?]) (or/c 'internal 'external)]{ + +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])]{ -Opens or activates the developer tools for the underlying webview. +@defmethod[(add-class! [selector-or-id (or/c symbol? string?)] + [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])]{ -Moves the native window to the given screen coordinates. +@defmethod[(remove-class! [selector-or-id (or/c symbol? string?)] + [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])]{ -Resizes the native window to the given dimensions. +@defmethod[(devtools) (is-a?/c wv-window%)]{ + +Opens the developer tools and returns this window. } -@defmethod*[([(close) this])]{ -Closes the native window. +@defmethod[(move [x exact-integer?] [y exact-integer?]) (is-a?/c wv-window%)]{ + +Moves the window and returns this window. } -@defmethod*[([(bind! [selector string?] - [events (or/c symbol? (listof symbol?))] - [callback procedure?]) (listof any/c)])]{ -Binds one or more DOM events to all elements matched by @racket[selector]. +@defmethod[(resize [w exact-integer?] [h exact-integer?]) (is-a?/c wv-window%)]{ -This method delegates the JavaScript side of the binding to @racket[webview-bind!], -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. +Resizes the window and returns this window. } -@defmethod*[([(unbind! [selector string?] - [events (or/c symbol? (listof symbol?))]) any/c])]{ -Removes previously installed bindings for the selected elements and events. -This method delegates to @racket[webview-unbind!] and removes callbacks from the -corresponding cached wrapper objects. +@defmethod[(close) (is-a?/c wv-window%)]{ + +Closes the window and returns this window. } -@defmethod*[([(file-dialog-done [flag symbol?] - [file any/c] - [dir any/c] - [filter any/c]) void?])]{ -Continuation hook used internally to resume a pending file or directory dialog. -Applications normally do not call this method directly. +@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*[([(choose-dir [title string?] [base-dir (or/c #f path-string?)]) - (or/c 'showing path-string? #f)])]{ -Shows a directory chooser. +@defmethod[(unbind! [selector (or/c symbol? string?)] + [events (or/c symbol? list?)]) + list?]{ -If the chooser is successfully shown, the method initially returns @racket['showing] -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]. -Only one file or directory dialog can be active at a time. +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*[([(file-open [title string?] - [base-dir (or/c #f path-string?)] - [filters any/c]) - (or/c 'showing list? #f)])]{ -Shows a file-open dialog. +@defmethod[(set-title! [title string?]) any/c]{ -If the dialog is accepted, the resumed result is a list whose contents are derived from -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. +Sets the window title. } -@defmethod*[([(file-save [title string?] - [base-dir (or/c #f path-string?)] - [filters any/c]) - (or/c 'showing list? #f)])]{ -Shows a file-save dialog. +@defmethod[(file-dialog-done [flag symbol?] + [file any/c] + [dir any/c] + [filter any/c]) + any/c]{ -If the dialog is accepted, the resumed result is a list whose contents are derived from -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. +Internal continuation callback used by file and directory dialogs. + +The default implementation resumes the pending continuation stored by the window. } -@defmethod*[([(message-done [event symbol?]) void?])]{ -Continuation hook used internally to resume a pending message box. -Applications normally do not call this method directly. +@defmethod[(choose-dir [title string?] [base-dir (or/c path-string? #f)]) + (or/c 'showing path-string? #f)]{ + +Shows a directory chooser dialog. + +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{the selected directory is returned on success} + @item{@racket[#f] is returned if the dialog was canceled}] } -@defmethod*[([(message [type symbol?] - [title string?] - [message string?] - [#:sub submessage string? ""]) - (or/c 'showing symbol? #f)])]{ -Shows a native message box. +@defmethod[(file-open [title string?] + [base-dir (or/c path-string? #f)] + [filters any/c]) + (or/c 'showing list? #f)]{ -If the message box is shown successfully, the method initially returns @racket['showing] -and later resumes with the button result, such as @racket['msgbox-ok], -@racket['msgbox-cancel], @racket['msgbox-yes], or @racket['msgbox-no]. -If the message box cannot be shown, the method returns @racket[#f]. -Only one message box can be active at a time. +Shows an open-file dialog. + +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*[([(init-size) void?])]{ -Initializes the window position and size from the settings object. -If no persisted values are available, constructor arguments or module defaults are used. +@defmethod[(file-save [title string?] + [base-dir (or/c path-string? #f)] + [filters any/c]) + (or/c 'showing list? #f)]{ + +Shows a save-file dialog. + +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 [evt symbol?]) any/c]{ + +Internal continuation callback used by message boxes. + +The default implementation resumes the pending message-box continuation stored by +the window. } -@defproc[(root-file-not-found-handler [standard-file path-string?] - [not-found-handler procedure?]) +@defmethod[(message [type symbol?] + [title string?] + [message string?] + [#:sub submessage string? ""]) + (or/c 'showing symbol? #f)]{ + +Shows a message box. + +At most one message box can be active at a time. Attempting to show another one +raises an exception. + +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) any/c]{ + +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} + +The window installs an internal event handler when it is created. + +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?]{ -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, -@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 -supplied. +Creates a file-not-found handler for use with file serving. + +The returned procedure has the shape: + +@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]{ -The version identifier exported by the underlying webview binding. -} +@section{Re-exports} + +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]. \ No newline at end of file