Files
racket-webview/scrbl/wv-window.scrbl
2026-04-01 16:23:56 +02:00

488 lines
15 KiB
Racket

#lang scribble/manual
@(require racket/base
racket/class
scribble/core
(for-label racket/base
racket/string
racket/class
racket/file
net/url
"../private/wv-context.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]
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% ()]{
Represents one window in a webview context.
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 (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 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])]{
Creates a window.
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 supplied at construction.
}
@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 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?] [type symbol?] ...) any/c]{
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 [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 [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 [st symbol?]) any/c]{
Called when the underlying window state changes.
The default implementation returns @racket[#t].
}
@defmethod[(page-loaded [oke any/c]) any/c]{
Called when a @racket['page-loaded] event is received.
The default implementation returns @racket[#t].
}
@defmethod[(can-close?) any/c]{
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[(closed) any/c]{
Called when a @racket['closed] event is received.
The default implementation returns @racket[#t].
}
@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[(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[(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[(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[(devtools) (is-a?/c wv-window%)]{
Opens the developer tools and returns this window.
}
@defmethod[(move [x exact-integer?] [y exact-integer?]) (is-a?/c wv-window%)]{
Moves the window and returns this window.
}
@defmethod[(resize [w exact-integer?] [h exact-integer?]) (is-a?/c wv-window%)]{
Resizes the window and returns this window.
}
@defmethod[(close) (is-a?/c wv-window%)]{
Closes the window and returns this window.
}
@defmethod[(bind! [selector (or/c symbol? string?)]
[events (or/c symbol? list?)]
[callback procedure?])
list?]{
Binds one or more DOM events.
The method first delegates to @racket[webview-bind!]. For each returned binding,
it connects the corresponding element wrapper to @racket[callback].
If @racket[events] is a symbol, it is treated as a single-element list.
The result is the list produced by mapping over the binding items. In practice,
this yields the corresponding cached element wrapper objects.
}
@defmethod[(unbind! [selector (or/c symbol? string?)]
[events (or/c symbol? list?)])
list?]{
Removes one or more DOM event bindings.
The method first delegates to @racket[webview-unbind!], then disconnects the
corresponding callbacks from cached element wrappers.
If an element wrapper no longer has any event callbacks, it is removed from the
internal cache.
The returned value is the list produced by @racket[webview-unbind!].
}
@defmethod[(set-menu! [menu is-wv-menu?])
(is-a?/c wv-window%)]{
Installs @racket[menu] in this window and returns this window.
The accepted argument follows the contract of @racket[webview-set-menu!]. The
method delegates to:
@racketblock[
(webview-set-menu! wv menu)
]
}
@defmethod[(connect-menu! [id symbol?] [callback procedure?])
(is-a?/c wv-window%)]{
Connects @racket[callback] to the menu item identified by @racket[id].
The method installs a binding for the @racket['menu-item-choosen] event by
delegating to:
@racketblock[
(send this bind! id 'menu-item-choosen
(λ (el evt data)
(callback)))
]
The callback is invoked without arguments when the corresponding menu item is
chosen.
}
@defmethod[(set-title! [title string?]) any/c]{
Sets the window title.
}
@defmethod[(file-dialog-done [flag symbol?]
[file any/c]
[dir any/c]
[filter any/c])
any/c]{
Internal continuation callback used by file and directory dialogs.
The default implementation resumes the pending continuation stored by the window.
}
@defmethod[(choose-dir [title string?] [base-dir (or/c 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[(file-open [title string?]
[base-dir (or/c path-string? #f)]
[filters any/c])
(or/c 'showing list? #f)]{
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[(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.
}
@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-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.
}
@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].