local table = require("@neyuki/table")
local string = require("@neyuki/string")
local ui = require("@neyuki/ui")
local webview = {}
-- Web pages rendered by the operating system's web engine (WebView2 on
-- Windows, WKWebView on macOS, WebKitGTK on Linux) on top of the
-- `__webview_*` natives in src/vm/libs/webview.rs.
--
-- Views share the event loop of `@neyuki/ui`: `ui.poll` and `ui.run` pump
-- them, count them as open windows and deliver their events as tables with
-- `kind = "webview"` and a `type` of `message`, `navigate`, `load`, `title`
-- or `close`. Everything a page sends is untrusted input.
function checkString(s: string, name: string)
if (s == nil) then error(name .. " must be provided") end
if (type(s) != "string") then error(name .. " must be a string") end
end
function checkNumber(n: number, name: string)
if (n == nil) then error(name .. " must be provided") end
if (type(n) != "number") then error(name .. " must be a number") end
end
function checkOptionalString(s: string?, name: string)
if (s != nil) then checkString(s, name) end
end
function checkOptionalNumber(n: number?, name: string)
if (n != nil and type(n) != "number") then error(name .. " must be a number") end
end
function checkOptionalBoolean(b: boolean?, name: string)
if (b != nil and type(b) != "boolean") then error(name .. " must be a boolean") end
end
-- Whether a view can be created here: a display and the system web engine.
function webview.isAvailable(): boolean
return __webview_available()
end
-- Opens a window showing a web page. `options` may hold `title`, `width`,
-- `height`, `resizable`, `visible` (like `ui.open`), `url` or `html` (the
-- first page), `devtools`, `userAgent`, `initScript` (runs before the page's
-- own scripts), `autoplay`, `backgroundThrottling` (false keeps timers
-- running while hidden), `dataDir` (a persistent profile, so cookies survive
-- restarts) and `assets` = { dir, scheme? }, which serves the files under
-- `dir` at `webview.assetUrl(...)`.
function webview.open(options: {any}?): {any}
options = options ?? {}
if (type(options) != "table") then error("options must be a table") end
checkOptionalString(options.title, "title")
checkOptionalNumber(options.width, "width")
checkOptionalNumber(options.height, "height")
checkOptionalBoolean(options.resizable, "resizable")
checkOptionalBoolean(options.visible, "visible")
checkOptionalString(options.url, "url")
checkOptionalString(options.html, "html")
checkOptionalBoolean(options.devtools, "devtools")
checkOptionalString(options.userAgent, "userAgent")
checkOptionalString(options.initScript, "initScript")
checkOptionalBoolean(options.autoplay, "autoplay")
checkOptionalBoolean(options.backgroundThrottling, "backgroundThrottling")
checkOptionalString(options.dataDir, "dataDir")
if (options.assets != nil) then
if (type(options.assets) != "table") then error("assets must be a table") end
checkString(options.assets.dir, "assets.dir")
checkOptionalString(options.assets.scheme, "assets.scheme")
end
if ((options.width != nil and options.width <= 0) or (options.height != nil and options.height <= 0)) then
error("width and height must be positive")
end
local id = __webview_open(options)
local scheme = "app"
if (options.assets != nil and options.assets.scheme != nil) then scheme = options.assets.scheme end
local closed = false
local view = {}
view.id = id
function checkOpen()
if (closed) then error("webview is closed") end
end
function view.isOpen(self: {any}): boolean
return not closed
end
function view.close(self: {any}): ()
if (closed) then return end
closed = true
ui.untrack(id)
__webview_close(id)
end
function view.navigate(self: {any}, url: string): ()
checkOpen()
checkString(url, "url")
__webview_navigate(id, url)
end
function view.loadHtml(self: {any}, html: string): ()
checkOpen()
checkString(html, "html")
__webview_load_html(id, html)
end
function view.reload(self: {any}): ()
checkOpen()
__webview_reload(id)
end
-- Runs JavaScript in the page and does not wait for it.
function view.eval(self: {any}, js: string): ()
checkOpen()
checkString(js, "script")
__webview_eval(id, js)
end
-- Delivers `value` to the page as a `neyuki` event on `window`, with the
-- JSON text in `event.detail`.
function view.send(self: {any}, value: any): ()
checkOpen()
local text = json.encode(value)
__webview_eval(id, "window.dispatchEvent(new CustomEvent('neyuki', { detail: " .. json.encode(text) .. " }))")
end
-- The address the page is at.
function view.url(self: {any}): string
checkOpen()
return __webview_url(id)
end
function view.setTitle(self: {any}, title: string): ()
checkOpen()
checkString(title, "title")
__webview_set_title(id, title)
end
function view.size(self: {any}): (number, number)
checkOpen()
local w, h = __webview_size(id)
return w, h
end
function view.setSize(self: {any}, w: number, h: number): ()
checkOpen()
checkNumber(w, "width")
checkNumber(h, "height")
if (w <= 0 or h <= 0) then error("width and height must be positive") end
__webview_set_size(id, w, h)
end
function view.show(self: {any}): ()
checkOpen()
__webview_set_visible(id, true)
end
function view.hide(self: {any}): ()
checkOpen()
__webview_set_visible(id, false)
end
-- Forgets cookies, storage and caches (signs the user out).
function view.clearData(self: {any}): ()
checkOpen()
__webview_clear_data(id)
end
-- The address of `path` in the asset directory given to `webview.open`.
function view.assetUrl(self: {any}, path: string?): string
return webview.assetUrl(scheme, path)
end
ui.track(id, view)
return table.freeze(view)
end
-- The address of `path` in an asset directory served under `scheme`
-- (default "app"). It differs by platform, so build it with this.
function webview.assetUrl(scheme: string?, path: string?): string
checkOptionalString(scheme, "scheme")
checkOptionalString(path, "path")
local base = __webview_asset_base(scheme ?? "app")
local rest = path ?? ""
if (string.sub(rest, 1, 1) != "/") then rest = "/" .. rest end
return base .. rest
end
return table.freeze(webview)