Standard library · Graphics and games

gui.ui

The native UI runtime: shows a Salam page in a native window drawn by Salam itself, the way a browser shows the same page, with layout, Persian text, events and form controls.

import gui.ui

This package runs a page as a native desktop window that Salam draws itself. The compiler turns a page: into the same HTML document it writes for the web, with the page's CSS in a <style> element and event attributes such as onclick="add()" that name Salam functions. gui.ui is a small browser engine for exactly that output: it parses the document, computes styles with gui.css (the user agent style sheet, hover, focus, active and media rules), lays boxes out with gui.cssbox (block, flex and absolute positioning), breaks and shapes text with gui.typeset (inline elements, bidi, Persian shaping, list markers), paints with gui.paint and shows the result with gui.window.

A program registers its handlers with Handle and calls Run with a function that returns the page. After every handler the view function runs again (a compiled page re-runs only the parts whose state changed, see below); the new document is matched against the old one by position, tag, id and key, so hover, focus, scroll offsets, the caret and the values typed into inputs survive, and only the parts of the window that changed are painted again. RunHtml shows a fixed document. Render draws a page without a window, for tests and screenshots.

Form controls are drawn by the runtime and follow the page's CSS: buttons, text, password, number, email and search inputs with a caret, selection, clipboard and undo, checkboxes, radio groups, ranges, text areas, selects with a popup list, progress, meter, details with summary, and labels that focus or toggle their control. Links to http and https addresses open in the user's browser; a relative link opens another page registered with Page.

Things to know

  • Changes made through ById (text, classes, attributes) last until the next time the view runs; the view is the source of truth. Values typed into inputs are kept until the view writes a different value attribute.
  • Fonts come from the system font folders unless the program adds its own with AddFontFile or AddFontDir before the first page is shown, which also makes the output the same on every machine.
  • With SALAM_WINDOW_BACKEND=headless the window is drawn in memory. If SALAM_UI_SCRIPT then names a text file, Run follows its steps, one per line, and returns at quit or at the end: click SELECTOR, dblclick SELECTOR, hover SELECTOR, press SELECTOR, release SELECTOR, move X Y, key NAME (such as Tab, Enter, ctrl+a, shift+Tab), type TEXT, ime TEXT and commit TEXT (input method composition), drag X1 Y1 X2 Y2, tripleclick SELECTOR, sel (prints the selection, caret and text scroll of the focused control), scroll SELECTOR DY, resize W H, wait MS, dump (prints each element's tag, id, text and border box), hash, focus, reveal SELECTOR (scrolls the element into view as ScrollIntoView does), shot FILE.png and quit. Selectors are CSS selectors; lines starting with # or // are skipped. Without a script a headless Run draws once and returns.
  • CSS transform, transform-origin, translate, rotate and scale apply to painting, hit testing and repainting, and transitions of transform run on the animation clock. filter, backdrop-filter, text-shadow, outline-offset and dashed or dotted rounded borders are drawn as in a browser.
  • Boxes are painted the way Chromium paints them: each background layer is clipped to its background-clip box with the matching radius and sized, positioned and tiled from its background-origin box, so a gradient tile wraps into the border area and the padding-box, border-box double background draws a gradient border with round corners. border-image draws a gradient or image as a nine-slice, double borders draw two lines, dashes and dots are spaced like Chromium's, and inset shadows stay inside the padding box. inset, outset, groove and ridge borders use Chromium's darker and lighter tones of the border colour, with groove and ridge split into two halves. A table with border-collapse: collapse resolves each shared edge between cells, rows, row groups, columns and the table by the CSS conflict rules and paints it segment by segment over the cell backgrounds, with hidden removing an edge and the corners going to the winning border. backdrop-filter reads what is behind the element up to its backdrop root (the nearest ancestor with opacity, a filter or a backdrop filter), with mirrored edges, and fades with the element's opacity; damage that touches a filtered element repaints all of it.
  • Text inputs and text areas show the input method composition underlined at the caret, keep the caret in view by scrolling (a text area scrolls with the wheel too, and sideways with wrap="off"), move the caret visually in mixed Persian and English text, and select with the mouse, Shift with the arrow keys, a double click (a word) and a triple click (a line).
  • Long lists are cheap to scroll: scrolling only moves boxes, painting and repainting skip everything outside the visible area, and elements that stay off screen are not compared. A list of 10,000 rows is still laid out in full once.
  • CSS grid and tables are laid out by gui.cssbox the way Chrome lays them out: track sizing, named areas and auto placement for grid; column widths from content, colspan and rowspan, captions, header and footer groups, border-spacing, collapsed borders and vertical-align in cells for tables. In a page, table, header, main, footer, row, column (or cell) and caption build the same table as the HTML output. inline-table, inline-grid and inline-flex boxes sit in the line box like an inline block, sized to their content and aligned on their first row or first item baseline, as in Chromium.
  • A style attribute wins over every author rule whatever its specificity, as an inline style does in a browser, and the dynamic styles of a compiled page take the same place, so a value set from Salam code is never hidden by a selector. Inline blocks, images and controls follow vertical-align in the line box: baseline, sub, super, middle, text-top, text-bottom, lengths and percentages move them against the parent baseline, and top and bottom align them with the line box, which grows to fit them, giving the same positions as Chromium. background-clip: text paints the background only through the glyphs of the element and its descendants. Dotted borders of 3px or less are drawn as square dots, and in a collapsed table the wider of two edges in one line owns the corner between them, as in Chromium.
  • The layout tree is kept between view re-runs and restyles: each element keeps its layout box, only boxes whose style, text, children or content changed are laid out again, and the rest come from the layout cache. A click that changes one label on a 202 element page runs about 60% fewer layout instructions than a full layout.
  • A scroll container with a fixed height or maximum height and at least 200 block children lays out only the rows near what is visible (one viewport above and below) and stands in for the rest with their measured heights, or the average of the measured rows until they are seen. Scrolling, clicking, focus with the Tab key and keyed each rows work as before. Rows outside the window have no box until they scroll in, but Rect gives their estimated position, Focus and ScrollIntoView scroll them in, and a script step that clicks or hovers such a row scrolls it into view first, as a browser test driver does. On a 10,000 row list the first frame takes about 70 to 100 ms instead of about 600 ms, and a scroll frame about 15 to 18 ms instead of about 100 ms.

A window from a page

The compiler writes this for a page with handlers; it is shown here by hand. The headless script at the end clicks twice so the example also runs without a display.

gui-ui-guide-run.salam
import gui.ui
import str

mut count := 0
mut name := ""

func page(): str:
    ret `<!doctype html><html lang="en" dir="ltr"><head><title>Counter</title><style>button:hover{background:#dde}</style></head><body><h1 id="count">Count ` + count + `</h1><button id="add" onclick="add()">Add</button><input id="name" oninput="named(event)" placeholder="Name"><p>Hello ` + str.Replace(name, "<", "&lt;") + `</p><a href="https://salamlang.ir">site</a> <a href="/about">about</a></body></html>`
end

func about(): str:
    ret `<!doctype html><html><head><title>About</title></head><body><p>Made with Salam.</p><a href="/">back</a></body></html>`
end

func main:
    ui.Handle("add", (): count++ end)
    ui.Handle("named", ():
        ev := ui.CurrentEvent()
        name = ev.value
        println ev.kind, ev.target, ev.text, ev.key, ev.checked, ev.button, ev.modifiers, ev.x, ev.y, ev.delta_x, ev.delta_y
    end)
    ui.Page("/", (): ret page() end)
    ui.Page("/about", (): ret about() end)
    ui.SetUrlOpener((url: str):
        println "would open", url
        ret true
    end)
    ui.SetNavigator((url: str): ret url == "/never" end)
    if count > 0:
        ui.Run((): ret page() end)
        ui.RunHtml(`<p>A fixed page</p>`)
        ui.RunHtmlWith(`<p>A fixed page</p>`, ui.Options { title = "Fixed", width = 400.0, height = 300.0 })
        ui.Refresh()
        println ui.OpenUrl("https://salamlang.ir")
    end
    ui.RunWith((): ret page() end, ui.Options { title = "Counter", width = 480.0, height = 320.0, resizable = true, background = "#ffffff", headless = true, scale = 1.0, script = "click #add\nclick #add\nquit" })
    println "count", count, "opened", ui.OpenedUrls().len()
end
Outputcount 2 opened 0

Rendering without a window

Render a Persian card to a canvas at normal and double density, save it as PNG and time each phase. Rendering a page of 200 elements takes about 0.1 ms to parse, 2 ms to style, 20 ms to lay out and 23 ms to paint at 1024 by 768 on one desktop core; a hover change repaints only the hovered card.

gui-ui-guide-render.salam
import gui.ui
import os
import filepath

func main:
    _a := ui.AddFontFile("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
    _b := ui.AddFontDir("/usr/share/fonts/truetype/noto")
    doc := `<!doctype html><html lang="fa" dir="rtl"><head><style>.card{border:2px solid #36c;border-radius:10px;padding:12px;margin:12px;box-shadow:0 2px 6px rgba(0,0,0,.3)}</style></head><body><div class="card"><h2>سلام</h2><p>یک کارت با <b>متن فارسی</b>.</p></div></body></html>`
    mut cv := ui.Render(doc, 320, 200)
    println "size", cv.width, cv.height
    cv.Free()
    mut hi := ui.RenderScaled(doc, 320, 200, 2.0)
    println "hidpi", hi.width, hi.height
    hi.Free()
    path := filepath.Join(os.TempDir(), "gui-ui-render.png")
    println "png", ui.RenderPNG(doc, 320, 200, path)
    os.Remove(path)
    tree := ui.DumpHtml(doc, 320, 200)
    println "dump starts with body", tree.starts_with("body")
    p := ui.Measure(doc, 320, 200)
    println "elements", p.elements, "timed", p.parse_ms >= 0.0 and p.style_ms >= 0.0 and p.layout_ms >= 0.0 and p.paint_ms >= 0.0
end
Outputsize 320 200 hidpi 640 400 png true dump starts with body true elements 5 timed true

Compiled pages

A page built with --ui=native does not go through HTML or CSS text at all. The compiler compiles the page's styles into a gui.css program and writes a view that takes a Builder and creates the elements with direct calls: Open and Close for each element, Decls for the ids of its compiled declaration lists, Text, Id, Class, Attr and On with a Salam closure for each handler. RunCompiled builds the program once and runs the view again after every handler. The new tree goes through the same matching, restyling, layout, paint and events as an HTML view, so the two give the same pixels. Only style values that are runtime expressions are parsed, once each, through Dyn.

For the 200 element page in tools/bench/gui_ui.salam on one desktop core, a view re-run after a click (building the tree, matching, restyling, layout and the partial repaint) takes about 7.2 ms on the compiled path and 7.7 ms on the HTML path; layout and paint are shared and take most of it. Shaped text paragraphs whose text and style did not change are reused across re-runs on both paths.

gui-ui-guide-compiled.salam
import gui.ui
import gui.css

mut count := 0

func styles(): css.Program:
    mut p := css.NewProgram()
    _box := css.CompileDecls(p, "padding: 16px; font-family: sans-serif")
    _btn := css.CompileDecls(p, "padding: 6px 12px; border-radius: 6px")
    _hover := css.CompileDecls(p, "background: #dde")
    ret p
end

func page(b: ui.Builder):
    b.Title("Counter")
    b.Lang("en")
    b.Dir("ltr")
    b.Open("div")
    b.Decls(0, -1, -1, -1)
    b.Open("h1")
    b.Id("count")
    b.Text("Count " + count)
    b.Close()
    b.Open("button")
    b.Decls(1, 2, -1, -1)
    b.Id("add")
    b.Class("primary")
    b.Key("add")
    b.Attr("type", "button")
    b.BoolAttr("disabled", count > 5)
    b.On("click", (): count++ end)
    b.Text("Add")
    b.Close()
    b.Open("p")
    b.Dyn("width", (100 + count * 10) + "px")
    b.Text("Grows with the count")
    b.Close()
    b.Close()
end

func main:
    if count > 99:
        ui.RunCompiled((): ret styles() end, (b: ui.Builder): page(b) end)
    end
    ui.RunCompiledWith((): ret styles() end, (b: ui.Builder): page(b) end, ui.Options { width = 320.0, height = 200.0, headless = true, script = "click #add\nclick #add\nquit" })
    println "count", count
    mut cv := ui.RenderCompiled((): ret styles() end, (b: ui.Builder): page(b) end, 320, 200)
    println "canvas", cv.width, cv.height
    cv.Free()
end
Outputcount 2 canvas 320 200

Re-running only what changed

A compiled page does not run its whole view after a handler. The compiler puts every component instance, and every element, if or each outside a component that reads state, in its own Scope, and works out at compile time which mutable globals each scope reads. It gives each such global a change bit and sets the bit before every statement in the program that may write it (assignments, ++, methods such as push, passing it by reference). A component variable (a line name := value in its body) is classified at compile time: one the view shows and some code changes is state, and a write to it marks just that instance; one only handlers use is kept per instance but never re-runs anything; one nothing changes is a derived value, worked out again each time the instance runs. A value passed to a child component counts as shown, so the child runs again when only its parameters change. After a handler, timer or animation step the runtime reads the bits: when none is set nothing runs at all; otherwise only the scopes that read a changed global run again, outermost first, and a component instance runs again as a whole. When a scope's elements keep their shape the new text and attributes are copied into the existing elements, only those are restyled, and changed text is reshaped in place, so layout runs only when a size can change.

Component instances inside each are matched to their old selves by the row's key: the key attribute of the row's first element, or its id. The compiler opens such an instance with KeyedScope, so inserting, removing or reordering rows moves each instance's state with its row, and the keyed element keeps its control state (typed text, focus) and layout identity. Without a key, instances follow their position. The same component variables work on the web with salam page build: the page script renders each instance from its state and, after a handler, re-renders only the instances it wrote and patches their DOM in place. There too an instance inside each keeps its state by the row's key, and the parts of the page that read a mutable global are re-rendered after a handler that writes it.

Components talk to the ones around them without any runtime machinery. A parent can pass a function as a parameter (Counter: change = (v: int): sum += v end end) and the child calls it (change(count)); a child can send an event with emit("saved", value), which goes to the nearest use site with on saved = (v: T): ... end and then up, level by level, to the page, until a handler calls stop(). Both are resolved while the components are expanded, so the writes in the parent's lambda mark the parent's instance like any other write, and an event from a deep child re-runs only the instances whose state changed (tests/en/ui/events_bubble.salam prints the ScopeRuns counts). The same code runs on the web.

For a page of 200 elements where one counter changes (tools/bench/gui_ui.salam, on a loaded six-core desktop), a click took 3.4 to 4.0 ms when the whole view ran again and 0.5 to 0.7 ms when only the counter's scope ran. A click whose handler changes nothing took 3.1 to 3.8 ms before and 0.2 to 0.3 ms now; what is left is event dispatch and the pressed-state repaint. Writes the compiler cannot see, such as changes inside another package, need Invalidate.

Types

Options

struct Options
How a window is opened.
NameTypeDescription
widthf64 = 1024Window width in logical pixels.
heightf64 = 768Window height in logical pixels.
titlestr = ""A window title to use instead of the page's title.
resizablebool = trueWhether the user can resize the window.
backgroundstr = ""A CSS colour for the window behind the page, used instead of the page's own.
headlessbool = falseDraws the window in memory, for tests and servers.
scalef64 = 0A device scale factor to use instead of the screen's.
scriptstr = ""Scripted steps that a headless RunWith follows instead of waiting for input.
gui-ui-Run.salam
import gui.ui
import str

mut count := 0
mut name := ""

func page(): str:
    ret `<!doctype html><html lang="en" dir="ltr"><head><title>Counter</title><style>button:hover{background:#dde}</style></head><body><h1 id="count">Count ` + count + `</h1><button id="add" onclick="add()">Add</button><input id="name" oninput="named(event)" placeholder="Name"><p>Hello ` + str.Replace(name, "<", "&lt;") + `</p><a href="https://salamlang.ir">site</a> <a href="/about">about</a></body></html>`
end

func about(): str:
    ret `<!doctype html><html><head><title>About</title></head><body><p>Made with Salam.</p><a href="/">back</a></body></html>`
end

func main:
    ui.Handle("add", (): count++ end)
    ui.Handle("named", ():
        ev := ui.CurrentEvent()
        name = ev.value
        println ev.kind, ev.target, ev.text, ev.key, ev.checked, ev.button, ev.modifiers, ev.x, ev.y, ev.delta_x, ev.delta_y
    end)
    ui.Page("/", (): ret page() end)
    ui.Page("/about", (): ret about() end)
    ui.SetUrlOpener((url: str):
        println "would open", url
        ret true
    end)
    ui.SetNavigator((url: str): ret url == "/never" end)
    if count > 0:
        ui.Run((): ret page() end)
        ui.RunHtml(`<p>A fixed page</p>`)
        ui.RunHtmlWith(`<p>A fixed page</p>`, ui.Options { title = "Fixed", width = 400.0, height = 300.0 })
        ui.Refresh()
        println ui.OpenUrl("https://salamlang.ir")
    end
    ui.RunWith((): ret page() end, ui.Options { title = "Counter", width = 480.0, height = 320.0, resizable = true, background = "#ffffff", headless = true, scale = 1.0, script = "click #add\nclick #add\nquit" })
    println "count", count, "opened", ui.OpenedUrls().len()
end
Outputcount 2 opened 0

Event

struct Event
An event delivered to a handler.
NameTypeDescription
kindstr = ""The event name without on: click, input, keydown and so on.
targetstr = ""The id of the element the event happened on, or an empty string.
xf64 = 0Pointer x in logical pixels from the window's left edge.
yf64 = 0Pointer y in logical pixels from the window's top edge.
buttonint = 0The mouse button: 0 left, 1 middle, 2 right.
keystr = ""The key name as a browser reports it: a, Enter, ArrowLeft.
textstr = ""The text typed, for input events.
valuestr = ""The value of the target input, text area or select.
checkedbool = falseWhether the target checkbox or radio button is checked.
modifiersint = 0The modifier keys held, as the gui.window modifier flags.
delta_xf64 = 0Horizontal wheel movement.
delta_yf64 = 0Vertical wheel movement.

The example for this item is shown with the ones above: gui-ui-Run.salam

Profile

struct Profile
How long each phase of rendering a page took.
NameTypeDescription
parse_msf64 = 0Milliseconds spent parsing the HTML.
style_msf64 = 0Milliseconds spent computing styles.
layout_msf64 = 0Milliseconds spent on layout and text.
paint_msf64 = 0Milliseconds spent painting.
elementsint = 0How many elements the page has.
gui-ui-Render.salam
import gui.ui
import os
import filepath

func main:
    _a := ui.AddFontFile("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
    _b := ui.AddFontDir("/usr/share/fonts/truetype/noto")
    doc := `<!doctype html><html lang="fa" dir="rtl"><head><style>.card{border:2px solid #36c;border-radius:10px;padding:12px;margin:12px;box-shadow:0 2px 6px rgba(0,0,0,.3)}</style></head><body><div class="card"><h2>سلام</h2><p>یک کارت با <b>متن فارسی</b>.</p></div></body></html>`
    mut cv := ui.Render(doc, 320, 200)
    println "size", cv.width, cv.height
    cv.Free()
    mut hi := ui.RenderScaled(doc, 320, 200, 2.0)
    println "hidpi", hi.width, hi.height
    hi.Free()
    path := filepath.Join(os.TempDir(), "gui-ui-render.png")
    println "png", ui.RenderPNG(doc, 320, 200, path)
    os.Remove(path)
    tree := ui.DumpHtml(doc, 320, 200)
    println "dump starts with body", tree.starts_with("body")
    p := ui.Measure(doc, 320, 200)
    println "elements", p.elements, "timed", p.parse_ms >= 0.0 and p.style_ms >= 0.0 and p.layout_ms >= 0.0 and p.paint_ms >= 0.0
end
Outputsize 320 200 hidpi 640 400 png true dump starts with body true elements 5 timed true

Builder

struct Builder
What a compiled view receives to create its elements. It is a small handle passed by value; the tree it builds belongs to the runtime.
gui-ui-Builder.salam
import gui.ui
import gui.css

mut count := 0

func styles(): css.Program:
    mut p := css.NewProgram()
    _box := css.CompileDecls(p, "padding: 16px; font-family: sans-serif")
    _btn := css.CompileDecls(p, "padding: 6px 12px; border-radius: 6px")
    _hover := css.CompileDecls(p, "background: #dde")
    ret p
end

func page(b: ui.Builder):
    b.Title("Counter")
    b.Lang("en")
    b.Dir("ltr")
    b.Open("div")
    b.Decls(0, -1, -1, -1)
    b.Open("h1")
    b.Id("count")
    b.Text("Count " + count)
    b.Close()
    b.Open("button")
    b.Decls(1, 2, -1, -1)
    b.Id("add")
    b.Class("primary")
    b.Key("add")
    b.Attr("type", "button")
    b.BoolAttr("disabled", count > 5)
    b.On("click", (): count++ end)
    b.Text("Add")
    b.Close()
    b.Open("p")
    b.Dyn("width", (100 + count * 10) + "px")
    b.Text("Grows with the count")
    b.Close()
    b.Close()
end

func main:
    if count > 99:
        ui.RunCompiled((): ret styles() end, (b: ui.Builder): page(b) end)
    end
    ui.RunCompiledWith((): ret styles() end, (b: ui.Builder): page(b) end, ui.Options { width = 320.0, height = 200.0, headless = true, script = "click #add\nclick #add\nquit" })
    println "count", count
    mut cv := ui.RenderCompiled((): ret styles() end, (b: ui.Builder): page(b) end, 320, 200)
    println "canvas", cv.width, cv.height
    cv.Free()
end
Outputcount 2 canvas 320 200

Title

func Title(s: str)
Sets the window title, like a page's title.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Lang

func Lang(s: str)
Sets the page language, like lang on html.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Dir

func Dir(s: str)
Sets the page direction, ltr or rtl, like dir on html.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Open

func Open(tag: str)
Opens an element with the HTML tag the web build would use, such as div, h1, button or input, as the last child of the open element. Every Open needs a Close, empty elements included.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Decls

func Decls(base: int, hover: int, focus: int, active: int)
Gives the open element its compiled declaration lists: its own, and those for hover, focus and active. -1 means none.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Dyn

func Dyn(prop: str, value: str)
Adds a style value known only at runtime, such as a width computed from state. Each distinct value is parsed once and cached.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Id

func Id(s: str)
Sets the element's id.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Class

func Class(s: str)
Adds a class to the element.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Key

func Key(s: str)
Sets the key that keeps an element matched to its old self across re-runs when siblings move.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Attr

func Attr(name: str, value: str)
Sets an attribute such as href, type, value or placeholder.

The example for this item is shown with the ones above: gui-ui-Builder.salam

BoolAttr

func BoolAttr(name: str, value: bool)
Sets a boolean attribute such as checked or disabled when the value is true.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Text

func Text(s: str)
Adds text to the open element. Text added next to child elements forms one paragraph with them, as in HTML.

The example for this item is shown with the ones above: gui-ui-Builder.salam

On

func On(event: str, fn: func())
Runs a closure when an event of the given name reaches the element: click, dblclick, input, change, submit, keydown, keyup, focus, blur and the others, named like the HTML attribute without on. Events bubble; CurrentEvent describes the event inside the closure.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Close

func Close()
Closes the open element.

The example for this item is shown with the ones above: gui-ui-Builder.salam

Scope

func Scope(site: int, fn: func(Builder))
Builds part of the page in a closure that the runtime can run again on its own. site names the place in the view, registered with ScopeSite; the closure receives the builder and is run at once. A compiled page puts every component instance, and every element, if or each that reads state, in a scope.
gui-ui-Scope.salam
import gui.ui
import gui.css

mut count := 0
mut touched := 0 as i64
mut likes := Vector {} as Vector<int>

func take(w: int): i64:
    mut t := 0 as i64
    if w == 0:
        t = touched
        touched = 0
    end
    ret t
end

func sites():
    ui.ScopeSite(1, -1, 0, 1)
    ui.ScopeSite(2, 0, 0, 0)
end

func card(b: ui.Builder, name: str):
    b.Scope(2, (b: ui.Builder):
        k := b.Instance(0)
        if b.Fresh():
            if k < likes.len():
                likes[k] = 0
            else:
                likes.push(0)
            end
        end
        b.Open("button")
        b.Id(name)
        b.On("click", ():
            likes[k] += 1
            ui.TouchInstance(0, k)
        end)
        b.Text(name + " " + likes[k])
        b.Close()
    end)
end

func page(b: ui.Builder):
    b.Reactive(1, (w: int): ret take(w) end, (): sites() end)
    b.Open("div")
    b.Scope(1, (b: ui.Builder):
        b.Open("h1")
        b.Id("count")
        b.Text("Count " + count)
        b.Close()
    end)
    b.Open("button")
    b.Id("add")
    b.On("click", ():
        count++
        touched |= 1
    end)
    b.Text("Add")
    b.Close()
    b.Open("button")
    b.Id("redraw")
    b.On("click", (): ui.Invalidate() end)
    b.Text("Redraw")
    b.Close()
    card(b, "tea")
    card(b, "cake")
    b.Close()
end

func main:
    script := "click #add\nclick #add\nclick #tea\nclick #cake\nclick #cake\nclick #redraw\nquit"
    ui.RunCompiledWith((): ret css.NewProgram() end, (b: ui.Builder): page(b) end, ui.Options { width = 320.0, height = 200.0, headless = true, script = script })
    println "count", count, "tea", likes[0], "cake", likes[1]
    println "scope runs", ui.ScopeRuns()
end
Outputcount 2 tea 1 cake 2 scope runs 6

KeyedScope

func KeyedScope(site: int, key: str, fn: func(Builder))
Like Scope, but the scope is matched to the one built last time by site and key instead of by position, so the state of a component instance in a list row follows the row when rows move. A compiled page uses it for component instances in a row that has a key or id.
gui-ui-KeyedScope.salam
import gui.ui
import gui.css

mut names := Vector {} as Vector<str>
mut touched := 0 as i64
mut likes := Vector {} as Vector<int>

func take(w: int): i64:
    mut t := 0 as i64
    if w == 0:
        t = touched
        touched = 0
    end
    ret t
end

func sites():
    ui.ScopeSite(1, -1, 0, 1)
    ui.ScopeSite(2, 0, 0, 0)
end

func check():
    mut line := ""
    each n in names:
        if line.len() > 0:
            line += ", "
        end
        line += ui.ById(n).Text()
    end
    println line
end

func reverse_names():
    mut out := Vector {} as Vector<str>
    each n in names:
        out.insert(0, n)
    end
    names.clear()
    each n2 in out:
        names.push(n2)
    end
    out.free()
    touched |= 1
end

func row(b: ui.Builder, name: str):
    b.KeyedScope(2, name, (b: ui.Builder):
        k := b.Instance(0)
        if b.Fresh():
            if k < likes.len():
                likes[k] = 0
            else:
                likes.push(0)
            end
        end
        b.Open("li")
        b.Key(name)
        b.Id(name)
        b.On("click", ():
            likes[k] += 1
            ui.TouchInstance(0, k)
        end)
        b.Text(name + " " + likes[k])
        b.Close()
    end)
end

func page(b: ui.Builder):
    b.Reactive(1, (w: int): ret take(w) end, (): sites() end)
    b.Open("div")
    b.Open("button")
    b.Id("rev")
    b.On("click", (): reverse_names() end)
    b.Text("Reverse")
    b.Close()
    b.Open("button")
    b.Id("drop")
    b.On("click", ():
        names.remove_at(0)
        touched |= 1
    end)
    b.Text("Drop")
    b.Close()
    b.Open("button")
    b.Id("check")
    b.On("click", (): check() end)
    b.Text("Check")
    b.Close()
    b.Open("ul")
    b.Scope(1, (b: ui.Builder):
        each n in names:
            row(b, n)
        end
    end)
    b.Close()
    b.Close()
end

func main:
    names.push("tea")
    names.push("cake")
    names.push("milk")
    script := "click #tea\nclick #cake\nclick #cake\nclick #check\nclick #rev\nclick #check\nclick #drop\nclick #check\nquit"
    ui.RunCompiledWith((): ret css.NewProgram() end, (b: ui.Builder): page(b) end, ui.Options { width = 320.0, height = 200.0, headless = true, script = script })
end
Outputtea 1, cake 2, milk 0 milk 0, cake 2, tea 1 cake 2, tea 1

Reactive

func Reactive(words: int, take: func(int): i64, sites: func())
Turns on partial re-runs for a compiled view: the number of change words, a function that returns and clears one word of change bits, and a function that registers the sites once. The view calls it first. Without it the whole view runs after every handler.

The example for this item is shown with the ones above: gui-ui-Scope.salam

Instance

func Instance(comp: int): int
Inside a component's scope, the slot of this instance among the live instances of component comp. The slot stays the same when the parent re-runs, so the component's state can be stored per slot.

The example for this item is shown with the ones above: gui-ui-Scope.salam

Fresh

func Fresh(): bool
True while a component instance builds for the first time, when its state should be set from its declarations.

The example for this item is shown with the ones above: gui-ui-Scope.salam

Element

struct Element
An element of the current page found by its id. Every method looks the id up again, so an Element stays valid after the view runs again.
NameTypeDescription
idstr = ""The id the element is found by.
gui-ui-Start.salam
import gui.ui

mut clicks := 0

func view(): str:
    ret `<!doctype html><html><head><title>Start</title><style>.on{color:red}</style></head><body><p id="msg" class="on">Clicked ` + clicks + `</p><button id="btn" onclick="hit()">Hit</button><input id="field" value="abc"><input id="box" type="checkbox"></body></html>`
end

func main:
    ui.Handle("hit", (): clicks++ end)
    ok := ui.Start((): ret view() end, ui.Options { width = 320.0, height = 200.0, headless = true })
    println "started", ok, ui.CurrentWindow().Title(), ui.Step(0)
    ui.RunSteps("click #btn\nclick #field\ntype !")
    msg := ui.ById("msg")
    println "text", msg.Text(), "tag", msg.Tag(), "exists", msg.Exists()
    field := ui.ById("field")
    println "value", field.Value()
    field.SetValue("set")
    box := ui.ById("box")
    box.SetChecked(true)
    println "checked", box.Checked(), "value", field.Value()
    msg.AddClass("big")
    msg.RemoveClass("on")
    msg.SetAttr("title", "note")
    println "class", msg.HasClass("big"), msg.HasClass("on"), "attr", msg.Attr("title")
    msg.SetText("replaced until the next view")
    ui.ById("btn").Focus()
    ui.ById("btn").ScrollIntoView()
    ui.ById("btn").Click()
    _alive := ui.Step(0)
    println "clicks", clicks, msg.Text(), "red", msg.Style().color.r
    r := msg.Rect()
    println "has box", r.width > 0.0, r.height > 0.0, r.x >= 0.0, r.y >= 0.0
    println "frames", ui.FrameCount() > 0, "dump lines", ui.Dump().len() > 0
    ui.RunSteps("hover #btn")
    println "damage", ui.LastDamageCount() >= 0, ui.LastDamageArea() >= 0
    partial := ui.Frame().Hash()
    ui.RepaintAll()
    println "partial equals full", partial == ui.Frame().Hash()
    ui.Stop()
    _ok := ui.StartHtml(`<p>fixed</p>`, ui.Options { headless = true })
    ui.Stop()
end
Outputstarted true Start true text Clicked 1 tag p exists true value abc! checked true value set class true false attr note clicks 2 Clicked 2 red 255 has box true true true true frames true dump lines true damage true true partial equals full true

Exists

func Exists(): bool
Whether the page has an element with this id.

The example for this item is shown with the ones above: gui-ui-Start.salam

Tag

func Tag(): str
The element's tag name.

The example for this item is shown with the ones above: gui-ui-Start.salam

Text

func Text(): str
The element's text with its descendants'. The caller frees it.

The example for this item is shown with the ones above: gui-ui-Start.salam

SetText

func SetText(text: str)
Replaces the element's content with text, until the view runs again.

The example for this item is shown with the ones above: gui-ui-Start.salam

Value

func Value(): str
The value of an input, text area or select, as typed or chosen.

The example for this item is shown with the ones above: gui-ui-Start.salam

SetValue

func SetValue(value: str)
Sets the value of an input, text area or select and moves the caret to its end.

The example for this item is shown with the ones above: gui-ui-Start.salam

Checked

func Checked(): bool
Whether a checkbox or radio button is checked.

The example for this item is shown with the ones above: gui-ui-Start.salam

SetChecked

func SetChecked(is_on: bool)
Checks or unchecks a checkbox or radio button; checking a radio button unchecks the others of its group.

The example for this item is shown with the ones above: gui-ui-Start.salam

Focus

func Focus()
Moves the keyboard focus to the element and scrolls it into view the way Chromium does: nothing when it is fully visible, the nearest edge when it is partly visible, and centred when it is out of view. A row of a long list that has no box yet is scrolled to the same way.

The example for this item is shown with the ones above: gui-ui-Start.salam

ScrollIntoView

func ScrollIntoView()
Scrolls the element's scroll container so the element's top is at the top of the visible area, like scrollIntoView() in a browser, including rows of a long list that have no box yet. The new position is laid out at the next step.

The example for this item is shown with the ones above: gui-ui-Start.salam

Click

func Click()
Clicks the element as the user would: handlers run and links, labels, buttons and checkboxes do what they do.

The example for this item is shown with the ones above: gui-ui-Start.salam

HasClass

func HasClass(name: str): bool
Whether the element has a class.

The example for this item is shown with the ones above: gui-ui-Start.salam

AddClass

func AddClass(name: str)
Adds a class, until the view runs again.

The example for this item is shown with the ones above: gui-ui-Start.salam

RemoveClass

func RemoveClass(name: str)
Removes a class, until the view runs again.

The example for this item is shown with the ones above: gui-ui-Start.salam

Attr

func Attr(name: str): str
An attribute's value, or an empty string.

The example for this item is shown with the ones above: gui-ui-Start.salam

SetAttr

func SetAttr(name: str, value: str)
Sets an attribute, until the view runs again.

The example for this item is shown with the ones above: gui-ui-Start.salam

Style

func Style(): css.ComputedStyle
The element's computed style.

The example for this item is shown with the ones above: gui-ui-Start.salam

Rect

func Rect(): paint.Rect
The element's border box in logical window pixels. For a row of a long list that is outside the laid out window, and for anything inside it, the estimated box of the row: its estimated top from the measured or average heights, the list's content width and the estimated height.

The example for this item is shown with the ones above: gui-ui-Start.salam

Functions

RunCompiledWith

func RunCompiledWith(program: func(): css.Program, view: func(Builder), options: Options)
Like RunCompiled with options such as the size, the title or a headless script.

The example for this item is shown with the ones above: gui-ui-Builder.salam

RunCompiled

func RunCompiled(program: func(): css.Program, view: func(Builder))
Opens a window for a compiled page: a function that returns its style program and a view that builds the elements with a Builder. Runs until the window closes. This is what the compiler calls for a page built with --ui=native.

The example for this item is shown with the ones above: gui-ui-Builder.salam

RenderCompiled

func RenderCompiled(program: func(): css.Program, view: func(Builder), width: int, height: int): paint.Canvas
Draws a compiled page without a window into a new canvas of the given size. The caller frees the canvas.

The example for this item is shown with the ones above: gui-ui-Builder.salam

ById

func ById(id: str): Element
The element with an id, for handlers that change the page directly.

The example for this item is shown with the ones above: gui-ui-Start.salam

Handle

func Handle(name: str, fn: func())
Registers a Salam function under a name, so that event attributes such as onclick="name()", onclick="name(event)" and onclick="window['name']()" run it. Registering the same name again replaces the function.

The example for this item is shown with the ones above: gui-ui-Run.salam

CurrentEvent

func CurrentEvent(): Event
The event being handled, while a handler runs: its kind, the target's id, the pointer position, the button, the key, the text and the value of the target control.

The example for this item is shown with the ones above: gui-ui-Run.salam

Start

func Start(view: func(): str, options: Options): bool
Opens a window for a view function, shows its first frame and returns at once; drive it with Step or RunSteps. Returns false when no window can be opened.

The example for this item is shown with the ones above: gui-ui-Start.salam

StartHtml

func StartHtml(doc: str, options: Options): bool
Like Start for a fixed document.

The example for this item is shown with the ones above: gui-ui-Start.salam

Step

func Step(timeout_ms: int): bool
Handles the waiting window events, runs the view again when a handler ran, and paints what changed. Waits up to the given milliseconds for an event when none is waiting. Returns false once the window is closed.

The example for this item is shown with the ones above: gui-ui-Start.salam

Stop

func Stop()
Closes the window opened by Start or Run.

The example for this item is shown with the ones above: gui-ui-Start.salam

RunWith

func RunWith(view: func(): str, options: Options)
Opens a window for a view function with the given options and runs until the window closes. The view runs again after every handler.

The example for this item is shown with the ones above: gui-ui-Run.salam

Run

func Run(view: func(): str)
Opens a window for a view function, sized 1024 by 768 and titled from the page's title, and runs until the window closes. This is what the compiler calls for a page.

The example for this item is shown with the ones above: gui-ui-Run.salam

RunHtmlWith

func RunHtmlWith(doc: str, options: Options)
Shows a fixed document in a window with the given options until the window closes.

The example for this item is shown with the ones above: gui-ui-Run.salam

RunHtml

func RunHtml(doc: str)
Shows a fixed document in a window until the window closes.

The example for this item is shown with the ones above: gui-ui-Run.salam

Refresh

func Refresh()
Asks for the view to run again before the next frame, for changes made outside a handler.

The example for this item is shown with the ones above: gui-ui-Run.salam

Render

func Render(doc: str, width: int, height: int): paint.Canvas
Draws a document without a window into a new canvas of the given size, at one device pixel per CSS pixel. The caller frees the canvas.

The example for this item is shown with the ones above: gui-ui-Render.salam

RenderScaled

func RenderScaled(doc: str, width: int, height: int, scale: f64): paint.Canvas
Like Render at a scale factor, for high density screens: the canvas has the size times the scale.

The example for this item is shown with the ones above: gui-ui-Render.salam

RenderPNG

func RenderPNG(doc: str, width: int, height: int, path: str): bool
Renders a document and saves it as a PNG file. Returns false when the file cannot be written.

The example for this item is shown with the ones above: gui-ui-Render.salam

Dump

func Dump(): str
A text outline of the current window's page: one line per shown element with its tag, id, own text, control value or state, focus and border box. The caller frees it.

The example for this item is shown with the ones above: gui-ui-Start.salam

DumpHtml

func DumpHtml(doc: str, width: int, height: int): str
The same outline for a document rendered without a window.

The example for this item is shown with the ones above: gui-ui-Render.salam

Measure

func Measure(doc: str, width: int, height: int): Profile
Renders a document once without a window and reports how long each phase took.

The example for this item is shown with the ones above: gui-ui-Render.salam

RunSteps

func RunSteps(steps: str)
Runs scripted steps, one per line, against the window opened by Start, using the same step language as SALAM_UI_SCRIPT. The clock is switched to manual time, so wait advances it.

The example for this item is shown with the ones above: gui-ui-Start.salam

RepaintAll

func RepaintAll()
Paints the whole window again. Tests use it to check that a partial repaint gave the same pixels.

The example for this item is shown with the ones above: gui-ui-Start.salam

LastDamageCount

func LastDamageCount(): int
How many damage rectangles the last frame painted; 0 means the whole window.

The example for this item is shown with the ones above: gui-ui-Start.salam

LastLayoutCalls

func LastLayoutCalls(): int
How many boxes the last layout computed instead of taking from the cache; after a view re-run that changes one text it counts only that text's box and its ancestors.

The example for this item is shown with the ones above: gui-ui-Start.salam

LastDamageArea

func LastDamageArea(): int
How many device pixels the last frame painted.

The example for this item is shown with the ones above: gui-ui-Start.salam

Frame

func Frame(): paint.Canvas
The canvas the window shows. It belongs to the runtime.

The example for this item is shown with the ones above: gui-ui-Start.salam

FrameCount

func FrameCount(): int
How many frames have been painted.

The example for this item is shown with the ones above: gui-ui-Start.salam

CurrentWindow

func CurrentWindow(): window.Window
The gui.window window that shows the page.

The example for this item is shown with the ones above: gui-ui-Start.salam

Page

func Page(path: str, view: func(): str)
Registers another view for a path, so a link with that relative address opens it in the same window.

The example for this item is shown with the ones above: gui-ui-Run.salam

SetUrlOpener

func SetUrlOpener(opener: func(str): bool)
Replaces the function that opens http, https and mailto links, for tests or for an app that wants to handle them itself.

The example for this item is shown with the ones above: gui-ui-Run.salam

SetNavigator

func SetNavigator(navigator: func(str): bool)
Sets a function that sees every relative link first; when it returns true the link is handled and the registered pages are not searched.

The example for this item is shown with the ones above: gui-ui-Run.salam

OpenedUrls

func OpenedUrls(): Vector<str>
Every link address followed so far, in order. It belongs to the runtime.

The example for this item is shown with the ones above: gui-ui-Run.salam

OpenUrl

func OpenUrl(url: str): bool
Opens an address in the user's browser: xdg-open on Linux, open on macOS and ShellExecuteW on Windows. Nothing is opened when SALAM_UI_NO_BROWSER is set or the address is empty.

The example for this item is shown with the ones above: gui-ui-Run.salam

AddFontFile

func AddFontFile(path: str): int
Adds the faces of a font file to the fonts pages use and returns how many were added.

The example for this item is shown with the ones above: gui-ui-Render.salam

AddFontDir

func AddFontDir(dir: str): int
Adds every font file in a folder and its subfolders and returns how many faces were added.

The example for this item is shown with the ones above: gui-ui-Render.salam

ScopeSite

func ScopeSite(site: int, comp: int, word: int, mask: i64)
Registers what a site depends on: the component it belongs to (-1 for none) and a mask of change bits in one word. A site that registers several words is called once per word.

The example for this item is shown with the ones above: gui-ui-Scope.salam

TouchInstance

func TouchInstance(comp: int, slot: int)
Marks one component instance as changed, so it alone re-runs after the handler. The compiler writes this before every write to a component variable that the view shows.

The example for this item is shown with the ones above: gui-ui-Scope.salam

Invalidate

func Invalidate()
Runs the whole view again after the current handler. Use it when state changed in a way the compiler cannot see: inside another package, through a pointer, or through a second name for the same Vector.

The example for this item is shown with the ones above: gui-ui-Scope.salam

ScopeRuns

func ScopeRuns(): int
How many scopes have run again since the page started; a whole view run counts as one. Tests print it to check that a click re-ran only what it should.

The example for this item is shown with the ones above: gui-ui-Scope.salam