slate

slate:window

A desktop window with a web page in it, and a bridge between that page and the program — the platform’s own web view, so nothing is bundled and a window weighs what the system already loaded.

This module is behind the webview build feature, and it is the one feature that is not on by default. The released binary and the Homebrew formula are unchanged by it; a desktop build is sysl build . --features webview, and the library it needs is brew install sysl-lang/tap/webview. A build without it says so when a program imports this module, naming the feature rather than claiming slate has no such module:

import { window } from slate:window
error: `slate:window` is not in this build -- it is behind the `webview` feature, so build with `--features webview`

A window in fourteen lines

import { window, windowBind, windowEval, windowDone } from slate:window

val w = window({ title: "Notes", width: 720, height: 520, html: page })

windowBind(w, "save", (text) -> writeFileSync("notes.txt", text))
windowBind(w, "load", () -> readFileSync("notes.txt").value ?? "")

// An ordinary slate timer, writing into the page while the window is up.
setInterval(() -> windowEval(w, "clock.textContent = new Date().toLocaleTimeString()"), 1000)

await windowDone(w)

print("window closed")

The blocks on this page are quoted rather than run, except the refusal above. Every one of them opens a window, and a window has to be looked at: dev/slatelang/slate/tests_window.sysl runs what can be checked without one, and examples/desktop/notes.sl is the program to run by hand.

Why it is here, and why it did not need a thread

A program that already runs on libuv gains a window without changing how it runs. slate is one thread with an event loop on it; a platform’s window system is also a loop, and the ordinary way to have both is a second thread with a lock between them. This module has neither. The platform’s loop is turned over from a libuv timer — eight milliseconds at a time, about 120 Hz — so every callback a page makes lands on slate’s own thread by construction.

What that buys is that nothing about the rest of the program changes. A server keeps serving while a window is up. A for await over a stream keeps stepping. A timer keeps firing and can write into the page, which is what the setInterval above is doing. None of it is arranged; it is what falls out of the window being a guest on the loop the program already had.

The window keeps the program alive exactly as a listening socket does. While one is open the program does not exit; when the last one closes, the timer stops and the program ends on its own.

A window is an integer

window(...) answers a number into a table inside slate, which is what every handle in slate is: a socket, a timer, a store and a child process are all numbers. So == compares what slate says it compares, print has something to say, and a number that names no window is refused by the table it is looked up in.

import { windowTitle } from slate:window

windowTitle(4, "hello")
`windowTitle` was given something that is not a window

Making one

window(options) takes an object and every member of it is optional:

optionwhat it is
titlewhat the title bar says
width, heighthow big the window is, in points — 800 by 600 by default
htmlthe page, as markup the program holds
urlthe page, as somewhere to load it from
debugopen the platform’s web inspector with the window

html and url are alternatives and naming both is refused, a window showing one page. First wins and last wins would each throw away something somebody wrote, which is the rule a manifest’s repeated key already follows.

val w = window({ title: "Notes", width: 720, height: 520, url: "http://localhost:8080/" })

Everything else can be set afterwards, and a window is an ordinary thing to change while it is up: windowTitle(w, text), windowSize(w, width, height), windowNavigate(w, url) and windowHtml(w, markup).

Talking to the page

windowBind(w, name, fn) makes name a function on the page’s window object, and it is asynchronous there: the page writes await save(text) and gets a promise.

The page’s arguments arrive as slate values — JSON on the wire, and the same reader parseJSON uses — and what the handler answers goes back as the page’s resolved value:

windowBind(w, "add", (a, b) -> a + b)
windowBind(w, "settings", () -> { theme: "dark", size: 14 })
await add(2, 3)        // 5
await settings()       // { theme: "dark", size: 14 }

A handler may be async, and that is the point of the design. It is answered whenever it is ready — a turn later, a file read later, a request later — and the page’s promise stays pending until then, which is what a page asking for something that takes time needs:

windowBind(w, "search", async (q) -> (await fetch("https://example.com/s?q=" + q)).value.body)

A fault in a handler rejects the page’s promise and does not stop the program. A page asking for something impossible is a bad request, and the program is a server to it:

try { await save(42) } catch (e) { console.log(e) }   // the slate fault's own sentence

Running JavaScript, and ending

windowEval(w, js) runs a script in the page and answers nothing: the page’s own thread runs it, and anything the program wants back comes through a bound function. That asymmetry is the platform’s rather than slate’s.

windowClose(w) closes the window as the user closing it would; closing one that has already gone is not a failure, which is what close on a socket does. windowClosed(w) answers whether it has gone, and windowDone(w) answers a promise that settles when it does — the idiom child.exited already uses, and what a program awaits instead of polling.

It is the interpreter only

Every name here refuses under slate js, and that is a shape case rather than something owed. A browser page is already inside a window and cannot make a native one — what it has is window.open, a second tab a pop-up blocker governs, with no title bar to set and no way to bind a function into itself. node has no window system in its standard library either: Electron, Tauri and node-gui are separate runtimes rather than modules, so a program compiled by slate js would depend on something the tool cannot see and did not install.

`window` opens a native window, and no JavaScript host has one to open -- a browser page is already
inside a window and node has no window system, so this module runs in the interpreter only

What is not here

No component model, no router, and no way to write a page in slate rather than in HTML. Each of those is a design decision with several defensible answers, and this module is a door onto somebody else’s window system with no policy of its own — which is slate:lmdb‘s tier exactly. What sits on top is a package’s job, and lath is the one already written.

Search

Esc
to navigate to open Esc to close