Packages

window

Open native windows that show std:ui interfaces, with menus, shortcuts, dialogs, the clipboard and screenshots.

import "std:window"

std:window opens a native window and draws a ui view in it. You write the interface once, with tags and a class for its state, and the same code can run in a window or as a web page with serve.

Windows are drawn with SDL2, which has to be installed where the program is built and run (libSDL2 on Linux, sdl2 from Homebrew on macOS, SDL2.dll beside the program on Windows). A window needs a display, so the examples on this page compile but don't run in the browser. Run them on your own machine.

Your first window#

A counter. The class holds the state, view() returns the interface, and show gives it to a window. window.run() keeps the window drawn until it is closed:

main.osl
import "std:ui"
import "std:window"

class Counter (
  int count = 0

  def add() (
    self.count += 1
  )

  def view() ui.node (
    return <column pad=24 gap=12 align=center>
      <text size=28 weight=bold>${self.count}</text>
      <button on:click=self.add>Add one</button>
    </column>
  )
)

*window.Window main = window.create({title: "Counter", width: 300, height: 200}).unwrap()
main.show(def() -> Counter())
window.run()
Click the code to edit it

Click the button, and the number changes. Make the window 600 by 400, change the title, then run it again.

What you can show#

show takes the same targets as app.UI in serve, and the type decides how it behaves:

  • A function returning a ui.node is a view, drawn again after every action.
  • A function returning an object with view() is a factory. It is called once for this window, so every window gets its own state.
  • An object is shared with everything else it is given to, other windows and web pages included.
main.osl
import "std:ui"
import "std:window"

class Note (
  string text = ""
  def view() ui.node (
    return <input bind=self.text placeholder="Write here"/>
  )
)

Note shared = Note()
*window.Window a = window.create({title: "A", width: 300, height: 120}).unwrap()
*window.Window b = window.create({title: "B", width: 300, height: 120}).unwrap()
a.show(shared)
b.show(shared)
window.run()
Click the code to edit it

Type in one window and the other follows. Swap shared for def() -> Note() in both calls and they stop sharing.

Quick check

You pass def() -> Counter() to show for two windows. What do they share?

Window options#

window.create takes title, width and height (800 by 600), x and y (centred if left out), resizable, borderless, fullscreen, hidden, alwaysOnTop, minWidth and minHeight. It returns a result, because there may be no display. Positions count from 1, like everything in OSL.

main.osl
import "std:window"

auto made = window.create({title: "Tool", width: 400, height: 300, alwaysOnTop: true})
if made.isErr() (
  log "no window: " ++ made.unwrapErr().message
  exit 1
)

*window.Window tool = made.unwrap()
tool.move(1, 1)
log tool.size() tool.bounds() tool.scale()
Click the code to edit it

size() is in units and scale() says how many pixels a unit is (2 on a high-density screen). title(), setTitle, resize, center, maximize, minimize, restore, setFullscreen, hide, unhide and close do what they say.

Keys and menus#

w.key("mod+s", fn) runs a function when the combination is pressed, mod being Cmd on macOS and Ctrl elsewhere. w.menu(items) draws a menu bar at the top of the window. Items can name the same shortcut:

main.osl
import "std:ui"
import "std:window"

class Editor (
  string text = ""
  string status = "Ready"

  def save() ( self.status = "Saved " ++ self.text.len ++ " characters" )
  def clear() ( self.text = "" )

  def view() ui.node (
    return <column pad=12 gap=8 width=fill>
      <input bind=self.text width=fill/>
      <text>${self.status}</text>
    </column>
  )
)

Editor editor = Editor()
*window.Window w = window.create({title: "Editor"}).unwrap()
w.show(editor)
w.key("mod+s", editor.save)
w.menu([
  {label: "File", items: [
    {label: "Save", action: editor.save, keys: "mod+s"},
    {separator: true},
    {label: "Clear", action: editor.clear}
  ]}
])
window.run()
Click the code to edit it

An action runs like a button's on:click, so the window updates afterwards. SDL2 has no native menu bars, so this one is drawn inside the window and looks the same on every system.

Asking the user#

w.ask(message, buttons) shows a native dialog and returns the label of the button pressed (or "" if it was closed). openFile, saveFile and pickFolder return a path, or null when the user cancels. All of them return a result, because a machine may have no dialog program:

main.osl
import "std:window"

*window.Window w = window.create({title: "Dialogs", width: 300, height: 100}).unwrap()

string answer = w.ask("Discard your changes?", ["Discard", "Keep"]).unwrap()
log "you chose " ++ answer

string? path = w.openFile({types: ["png", "jpg"]}).unwrap()
if path != null (
  log "opening " ++ path
)
Click the code to edit it

Closing#

window.run() returns when the last window closes. To stop a window closing, give it an onClose handler that returns false:

main.osl
import "std:window"

*window.Window w = window.create({title: "Unsaved"}).unwrap()
bool dirty = true

w.onClose(def() boolean (
  if dirty (
    return w.ask("Quit without saving?", ["Quit", "Stay"]).unwrap() == "Quit"
  )
  return true
))
window.run()
Click the code to edit it

Your own loop#

A game or a visualisation wants a frame loop of its own. Call window.update() once per frame instead of run(). It draws the windows, handles the events, waits for the next frame, and returns the milliseconds that passed:

main.osl
import "std:ui"
import "std:window"

class Clock (
  number seconds = 0
  def view() ui.node -> <text size=40>${round(self.seconds, 1)}</text>
)

Clock clock = Clock()
*window.Window w = window.create({title: "Clock", width: 200, height: 100}).unwrap()
w.show(clock)

while w.isOpen() (
  clock.seconds += window.update() / 1000
  if w.held("space") ( clock.seconds = 0 )
)
Click the code to edit it

w.held(name) says whether a key or "mouseLeft" is down now.

Screenshots and the clipboard#

w.screenshot(path) saves what the window shows as a PNG, at its full pixel size, which is how to take pictures for documentation. window.clipboard() and window.setClipboard(text) read and write the system clipboard:

main.osl
import "std:window"

*window.Window w = window.create({title: "Shot", width: 200, height: 100}).unwrap()
window.update()
w.screenshot("shot.png").unwrap()
window.setClipboard("copied").unwrap()
log window.clipboard()
Click the code to edit it
Your turn

A greeting window

Make a window titled "Greeter" with a text input and a line that says Hello, followed by what was typed. This page can't open a window, so the starter prints the greeting the component would show. Make it print Hello, Ada.

main.osl
Ctrl Enter runs
Expected output
Hello, Ada
Your turn

A window title helper

Windows are named with title. Write def titleFor(string file) string that returns "Editor - " ++ file, or "Editor" when the file name is empty. Print both.

main.osl
Ctrl Enter runs
Expected output
Editor - notes.txt
Editor

API reference#

43 members, generated from the std:window source of OSL 9.3.0.

Functions

#window.all() *window.Window[]

all lists the open windows, in the order they were opened.

#window.clipboard() string

clipboard is the text on the system clipboard, or "" when there is none or no display.

#window.create(object? options) result<*window.Window, error>

create opens a window. Options: title ("OSL"), width and height (800 by 600), x and y (centred when left out), resizable (true), borderless, fullscreen, hidden, alwaysOnTop (all false), minWidth and minHeight, parent (a *window.Window the new one is centred over and closes with) and closesWith (a *window.Window whose closing closes this one). Fails with RuntimeError when no window can be opened, such as on a machine without a display. Throws StateError off the main thread.

#window.displays() {height: int, name: string, scale: number, width: int, x: int, y: int}[]

displays lists the screens: each one's name, its area, counted from 1, and its scale. It is empty when there is no display.

#window.focused() *window.Window

focused is the open window that has the keyboard, or null.

#window.run()

run keeps every window drawn and answering until none is left open. Throws StateError off the main thread.

#window.setClipboard(string text) result<unit, error>

setClipboard puts text on the system clipboard. Fails with RuntimeError when there is no display to hold it.

#window.update() number

update draws every open window and answers the clicks, typing and other events that arrived since the last call, then returns how many milliseconds passed since it last returned. It waits for the next frame when nothing is happening, so a loop calling it runs at about 60 frames a second without using the processor while idle. Throws StateError off the main thread.

*window.Window

#window.ask(string message, array buttons) result<string, error>

ask shows message in a dialog with buttons, such as ["Save", "Cancel"], and returns the label of the one pressed, or "" when the dialog was closed another way. An empty array shows one OK button. It waits for the answer. The dialog is SDL's native message box on every platform. Fails with RuntimeError when no dialog can open.

While the dialog waits, other actions can run, such as a web page's click or a thread's then, as other code runs during a JavaScript await. Read the state you need again after it returns: in seen = self.count; w.ask(...); self.count = seen + 1, a click made while the dialog was open is lost.

#window.bounds() {height: int, width: int, x: int, y: int}

bounds is the window's position, counted from 1, and size together.

#window.center()

center puts the window in the middle of its screen.

#window.close()

close closes the window straight away, and every window that closes with it. Closing a closed window does nothing.

#window.content() any

content is what the window shows: the target given to show, or null.

#window.duplicate() result<*window.Window, error>

duplicate opens another window with this one's options, a little below and to the right, showing the same target: a shared object stays shared and a factory makes the new window its own. Fails like window.create.

#window.focus()

focus brings the window to the front and gives it the keyboard.

#window.held(string name) boolean

held reports whether a key, by the names key takes, or a mouse button, "mouseLeft", "mouseRight" or "mouseMiddle", is held down now.

#window.hide()

hide hides the window without closing it.

#window.isFocused() boolean

isFocused reports whether the window has the keyboard.

#window.isOpen() boolean

isOpen reports whether the window is still open.

#window.isVisible() boolean

isVisible reports whether the window is open and not hidden.

#window.key(string keys, any fn)

key runs fn when the key combination keys is pressed while the window has the keyboard, such as "mod+s", "ctrl+shift+z" or "f5". mod is Cmd on macOS and Ctrl elsewhere. fn runs like an action, so the window updates after it. Throws ValueError for a combination it doesn't know.

#window.maximize()

maximize makes the window as big as its screen allows.

#window.menu(array items)

menu sets the window's menu bar: an array of menus, each {label, items}, where an item is {label, action, keys} or {separator: true}. keys is a shortcut, as key takes, that runs the item's action and shows beside it. SDL2 has no native menus, so the bar is drawn at the top of the window on every platform. An empty array removes it. Throws TypeError for a menu or item of the wrong shape.

#window.minimize()

minimize shrinks the window to the taskbar or dock.

#window.move(int x, int y)

move puts the window's top-left corner at x, y on the screen, counted from 1.

#window.onClose(any fn)

onClose adds fn, which runs when the window is asked to close, by its close button or requestClose. The window closes unless a handler returns false.

#window.openFile(object? options) result<string?, error>

openFile asks the user to pick a file to open and returns its path, or null when they cancel. Option: types, the extensions to offer, such as ["png", "jpg"]. The dialog is zenity or kdialog on Linux, AppleScript on macOS and PowerShell on Windows; fails with NotFoundError when none of them is there.

While the dialog waits, other actions can run, such as a web page's click or a thread's then, as other code runs during a JavaScript await. Read the state you need again after it returns: in seen = self.count; w.openFile(); self.count = seen + 1, a click made while the dialog was open is lost.

#window.pickFolder() result<string?, error>

pickFolder asks the user to pick a folder and returns its path, or null when they cancel. Dialogs are as for openFile.

While the dialog waits, other actions can run, such as a web page's click or a thread's then, as other code runs during a JavaScript await. Read the state you need again after it returns: in seen = self.count; w.pickFolder(); self.count = seen + 1, a click made while the dialog was open is lost.

#window.position() {x: int, y: int}

position is where the window's top-left corner is on the screen, counted from 1.

#window.requestClose()

requestClose asks the window to close, as its close button does: it closes unless an onClose handler returns false.

#window.resize(int width, int height)

resize sets the window's size. Throws ValueError below 1 by 1.

#window.restore()

restore undoes minimize and maximize.

#window.saveFile(object? options) result<string?, error>

saveFile asks the user where to save a file and returns the path, or null when they cancel. Options: types, as openFile takes, and name, the file name to suggest. Dialogs are as for openFile.

While the dialog waits, other actions can run, such as a web page's click or a thread's then, as other code runs during a JavaScript await. Read the state you need again after it returns: in seen = self.count; w.saveFile(); self.count = seen + 1, a click made while the dialog was open is lost.

#window.scale() number

scale is how many pixels the window draws per unit: 2 on a typical high-density screen, 1 elsewhere.

#window.screenshot(string path) result<unit, error>

screenshot saves what the window shows as a PNG file at path, at its full pixel size. Fails with IOError when the file can't be written.

#window.setFullscreen(boolean on)

setFullscreen makes the window fill its screen, or not.

#window.setMinSize(int width, int height)

setMinSize sets the smallest size the user can resize the window to.

#window.setTitle(string title)

setTitle sets the window's title.

#window.shouldClose() boolean

shouldClose reports whether the window has been asked to close, by its close button or requestClose, whatever its onClose handlers decided.

#window.show(any target)

show makes the window show target, by the same rule as app.UI: a function returning ui.node is a view, a function returning an object with view() is a factory called once for this window, and an object is shared; the compiler checks target's type. Throws TypeError for a factory that takes a request, which a window doesn't have.

#window.size() {height: int, width: int}

size is the window's size, in units, not pixels; see scale.

#window.title() string

title is the window's title.

#window.unhide()

unhide shows a hidden window again.