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 differentvalueattribute. - Fonts come from the system font folders unless the program adds its own with
AddFontFileorAddFontDirbefore the first page is shown, which also makes the output the same on every machine. - With
SALAM_WINDOW_BACKEND=headlessthe window is drawn in memory. IfSALAM_UI_SCRIPTthen names a text file,Runfollows its steps, one per line, and returns atquitor at the end:click SELECTOR,dblclick SELECTOR,hover SELECTOR,press SELECTOR,release SELECTOR,move X Y,key NAME(such asTab,Enter,ctrl+a,shift+Tab),type TEXT,ime TEXTandcommit 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 asScrollIntoViewdoes),shot FILE.pngandquit. Selectors are CSS selectors; lines starting with#or//are skipped. Without a script a headlessRundraws once and returns. - CSS
transform,transform-origin,translate,rotateandscaleapply to painting, hit testing and repainting, and transitions oftransformrun on the animation clock.filter,backdrop-filter,text-shadow,outline-offsetand 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-clipbox with the matching radius and sized, positioned and tiled from itsbackground-originbox, so a gradient tile wraps into the border area and thepadding-box, border-boxdouble background draws a gradient border with round corners.border-imagedraws 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,grooveandridgeborders use Chromium's darker and lighter tones of the border colour, withgrooveandridgesplit into two halves. A table withborder-collapse: collapseresolves 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, withhiddenremoving an edge and the corners going to the winning border.backdrop-filterreads 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.cssboxthe way Chrome lays them out: track sizing, named areas and auto placement for grid; column widths from content,colspanandrowspan, captions, header and footer groups,border-spacing, collapsed borders andvertical-alignin cells for tables. In a page,table,header,main,footer,row,column(orcell) andcaptionbuild the same table as the HTML output.inline-table,inline-gridandinline-flexboxes 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
styleattribute 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 followvertical-alignin the line box:baseline,sub,super,middle,text-top,text-bottom, lengths and percentages move them against the parent baseline, andtopandbottomalign them with the line box, which grows to fit them, giving the same positions as Chromium.background-clip: textpaints 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
eachrows work as before. Rows outside the window have no box until they scroll in, butRectgives their estimated position,FocusandScrollIntoViewscroll 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.
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, "<", "<") + `</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()
endRendering 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.
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
endCompiled 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.
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()
endRe-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| Name | Type | Description |
|---|---|---|
width | f64 = 1024 | Window width in logical pixels. |
height | f64 = 768 | Window height in logical pixels. |
title | str = "" | A window title to use instead of the page's title. |
resizable | bool = true | Whether the user can resize the window. |
background | str = "" | A CSS colour for the window behind the page, used instead of the page's own. |
headless | bool = false | Draws the window in memory, for tests and servers. |
scale | f64 = 0 | A device scale factor to use instead of the screen's. |
script | str = "" | Scripted steps that a headless RunWith follows instead of waiting for input. |
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, "<", "<") + `</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()
endEvent
struct Event| Name | Type | Description |
|---|---|---|
kind | str = "" | The event name without on: click, input, keydown and so on. |
target | str = "" | The id of the element the event happened on, or an empty string. |
x | f64 = 0 | Pointer x in logical pixels from the window's left edge. |
y | f64 = 0 | Pointer y in logical pixels from the window's top edge. |
button | int = 0 | The mouse button: 0 left, 1 middle, 2 right. |
key | str = "" | The key name as a browser reports it: a, Enter, ArrowLeft. |
text | str = "" | The text typed, for input events. |
value | str = "" | The value of the target input, text area or select. |
checked | bool = false | Whether the target checkbox or radio button is checked. |
modifiers | int = 0 | The modifier keys held, as the gui.window modifier flags. |
delta_x | f64 = 0 | Horizontal wheel movement. |
delta_y | f64 = 0 | Vertical wheel movement. |
The example for this item is shown with the ones above: gui-ui-Run.salam
Profile
struct Profile| Name | Type | Description |
|---|---|---|
parse_ms | f64 = 0 | Milliseconds spent parsing the HTML. |
style_ms | f64 = 0 | Milliseconds spent computing styles. |
layout_ms | f64 = 0 | Milliseconds spent on layout and text. |
paint_ms | f64 = 0 | Milliseconds spent painting. |
elements | int = 0 | How many elements the page has. |
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
endBuilder
struct Builderimport 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()
endTitle
func Title(s: str)title.The example for this item is shown with the ones above: gui-ui-Builder.salam
Lang
func Lang(s: str)lang on html.The example for this item is shown with the ones above: gui-ui-Builder.salam
Dir
func Dir(s: str)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)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)The example for this item is shown with the ones above: gui-ui-Builder.salam
Dyn
func Dyn(prop: str, value: str)The example for this item is shown with the ones above: gui-ui-Builder.salam
Id
func Id(s: str)The example for this item is shown with the ones above: gui-ui-Builder.salam
Class
func Class(s: str)The example for this item is shown with the ones above: gui-ui-Builder.salam
Key
func Key(s: str)The example for this item is shown with the ones above: gui-ui-Builder.salam
Attr
func Attr(name: str, value: str)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)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)The example for this item is shown with the ones above: gui-ui-Builder.salam
On
func On(event: str, fn: func())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()The example for this item is shown with the ones above: gui-ui-Builder.salam
Scope
func Scope(site: int, fn: func(Builder))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.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()
endKeyedScope
func KeyedScope(site: int, key: str, fn: func(Builder))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.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 })
endReactive
func Reactive(words: int, take: func(int): i64, sites: func())The example for this item is shown with the ones above: gui-ui-Scope.salam
Instance
func Instance(comp: int): intcomp. 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(): boolThe example for this item is shown with the ones above: gui-ui-Scope.salam
Element
struct ElementElement stays valid after the view runs again.| Name | Type | Description |
|---|---|---|
id | str = "" | The id the element is found by. |
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()
endExists
func Exists(): boolThe example for this item is shown with the ones above: gui-ui-Start.salam
Tag
func Tag(): strThe example for this item is shown with the ones above: gui-ui-Start.salam
Text
func Text(): strThe example for this item is shown with the ones above: gui-ui-Start.salam
SetText
func SetText(text: str)The example for this item is shown with the ones above: gui-ui-Start.salam
Value
func Value(): strThe example for this item is shown with the ones above: gui-ui-Start.salam
SetValue
func SetValue(value: str)The example for this item is shown with the ones above: gui-ui-Start.salam
Checked
func Checked(): boolThe example for this item is shown with the ones above: gui-ui-Start.salam
SetChecked
func SetChecked(is_on: bool)The example for this item is shown with the ones above: gui-ui-Start.salam
Focus
func Focus()The example for this item is shown with the ones above: gui-ui-Start.salam
ScrollIntoView
func ScrollIntoView()The example for this item is shown with the ones above: gui-ui-Start.salam
Click
func Click()The example for this item is shown with the ones above: gui-ui-Start.salam
HasClass
func HasClass(name: str): boolThe example for this item is shown with the ones above: gui-ui-Start.salam
AddClass
func AddClass(name: str)The example for this item is shown with the ones above: gui-ui-Start.salam
RemoveClass
func RemoveClass(name: str)The example for this item is shown with the ones above: gui-ui-Start.salam
Attr
func Attr(name: str): strThe example for this item is shown with the ones above: gui-ui-Start.salam
SetAttr
func SetAttr(name: str, value: str)The example for this item is shown with the ones above: gui-ui-Start.salam
Style
func Style(): css.ComputedStyleThe example for this item is shown with the ones above: gui-ui-Start.salam
Rect
func Rect(): paint.RectThe 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)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))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.CanvasThe example for this item is shown with the ones above: gui-ui-Builder.salam
ById
func ById(id: str): ElementThe example for this item is shown with the ones above: gui-ui-Start.salam
Handle
func Handle(name: str, fn: func())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(): EventThe example for this item is shown with the ones above: gui-ui-Run.salam
Start
func Start(view: func(): str, options: Options): boolStep 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): boolStart 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): boolThe example for this item is shown with the ones above: gui-ui-Start.salam
Stop
func Stop()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)The example for this item is shown with the ones above: gui-ui-Run.salam
Run
func Run(view: func(): str)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)The example for this item is shown with the ones above: gui-ui-Run.salam
RunHtml
func RunHtml(doc: str)The example for this item is shown with the ones above: gui-ui-Run.salam
Refresh
func Refresh()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.CanvasThe 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.CanvasRender 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): boolThe example for this item is shown with the ones above: gui-ui-Render.salam
Dump
func Dump(): strThe example for this item is shown with the ones above: gui-ui-Start.salam
DumpHtml
func DumpHtml(doc: str, width: int, height: int): strThe example for this item is shown with the ones above: gui-ui-Render.salam
Measure
func Measure(doc: str, width: int, height: int): ProfileThe example for this item is shown with the ones above: gui-ui-Render.salam
RunSteps
func RunSteps(steps: str)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()The example for this item is shown with the ones above: gui-ui-Start.salam
LastDamageCount
func LastDamageCount(): intThe example for this item is shown with the ones above: gui-ui-Start.salam
LastLayoutCalls
func LastLayoutCalls(): intThe example for this item is shown with the ones above: gui-ui-Start.salam
LastDamageArea
func LastDamageArea(): intThe example for this item is shown with the ones above: gui-ui-Start.salam
Frame
func Frame(): paint.CanvasThe example for this item is shown with the ones above: gui-ui-Start.salam
FrameCount
func FrameCount(): intThe example for this item is shown with the ones above: gui-ui-Start.salam
CurrentWindow
func CurrentWindow(): window.Windowgui.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)The example for this item is shown with the ones above: gui-ui-Run.salam
SetUrlOpener
func SetUrlOpener(opener: func(str): bool)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)The example for this item is shown with the ones above: gui-ui-Run.salam
OpenedUrls
func OpenedUrls(): Vector<str>The example for this item is shown with the ones above: gui-ui-Run.salam
OpenUrl
func OpenUrl(url: str): boolxdg-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): intThe example for this item is shown with the ones above: gui-ui-Render.salam
AddFontDir
func AddFontDir(dir: str): intThe 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)The example for this item is shown with the ones above: gui-ui-Scope.salam
TouchInstance
func TouchInstance(comp: int, slot: int)The example for this item is shown with the ones above: gui-ui-Scope.salam
Invalidate
func Invalidate()The example for this item is shown with the ones above: gui-ui-Scope.salam
ScopeRuns
func ScopeRuns(): intThe example for this item is shown with the ones above: gui-ui-Scope.salam