neyuki 0.5.0

A small Lua-like scripting language implemented in Rust
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)