Browser GUI generator
For new readers
PatLang has no native windowing/GUI toolkit โ this demo shows its alternative: a PatLang program that generates a plain HTML+JavaScript page (using the browser itself as the display), which then renders live below. Click the button to see it happen in real time.
PatLang computing data (a fibonacci table) and generating a well-formed, interactive HTML+JS page around it โ the browser as PatLang's cross-platform GUI. Running live below: the iframe is the actual page this PatLang program just generated, in this browser, right now.
PatLang source
# =============================================================================
# HTML/JS page builder (Stage 1 compilable subset).
# Produces well-formed HTML5 documents with embedded JavaScript, for using
# the browser as PatLang's cross-platform GUI. Write pages with write_file
# or serve them with the tcp_* hosts.
#
# Conventions: use single quotes inside embedded JS/CSS/attributes so the
# escape-free Stage 1 dialect can express them directly; q() gives a double
# quote when one is unavoidable.
# =============================================================================
# Self-contained helpers (duplicated from lib/codegen.patlang so GUI programs
# need not pull in the compiler libraries)
make a function called q returns s
return chr(34)
end
make a function called nl returns s
return chr(10)
end
make a function called html_escape takes s returns out
let b = sb_new()
let i = 0
while i < s.length do
let c = char_code(s, i)
if c == 38 then
sb_push(b, "&")
else
if c == 60 then
sb_push(b, "<")
else
if c == 62 then
sb_push(b, ">")
else
if c == 34 then
sb_push(b, """)
else
sb_push(b, s[i])
end
end
end
end
let i = i + 1
end
return sb_str(b)
end
# Standard page chrome: dark-and-light friendly minimal styling
make a function called html_default_style returns s
let b = sb_new()
sb_push(b, ":root { color-scheme: light dark; }" + nl())
sb_push(b, "body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }" + nl())
sb_push(b, "h1 { font-size: 1.4rem; }" + nl())
sb_push(b, "button, input { font: inherit; padding: 0.3rem 0.7rem; }" + nl())
sb_push(b, "ul { padding-left: 1.2rem; }" + nl())
sb_push(b, ".card { border: 1px solid color-mix(in srgb, currentColor 25%, transparent); border-radius: 8px; padding: 1rem; margin: 1rem 0; }" + nl())
sb_push(b, "pre { overflow-x: auto; background: color-mix(in srgb, currentColor 8%, transparent); border-radius: 6px; padding: 0.6rem; font-size: 0.85rem; }" + nl())
sb_push(b, "pre.src { max-height: 22rem; }" + nl())
sb_push(b, "pre.out { min-height: 1.5rem; }" + nl())
sb_push(b, ".label { margin-bottom: 0.2rem; opacity: 0.75; font-size: 0.9rem; }" + nl())
sb_push(b, ".ms { opacity: 0.75; font-size: 0.9rem; }" + nl())
sb_push(b, "summary { cursor: pointer; }" + nl())
return sb_str(b)
end
# Well-formed HTML5 document: html_page(title, body_html, script_js)
make a function called html_page takes title, body, script returns out
let b = sb_new()
sb_push(b, "<!DOCTYPE html>" + nl())
sb_push(b, "<html lang='en'>" + nl())
sb_push(b, "<head>" + nl())
sb_push(b, "<meta charset='utf-8'>" + nl())
sb_push(b, "<meta name='viewport' content='width=device-width, initial-scale=1'>" + nl())
sb_push(b, "<title>" + html_escape(title) + "</title>" + nl())
sb_push(b, "<style>" + nl() + html_default_style() + "</style>" + nl())
sb_push(b, "</head>" + nl())
sb_push(b, "<body>" + nl())
sb_push(b, body + nl())
sb_push(b, "<script>" + nl())
sb_push(b, "'use strict';" + nl())
sb_push(b, script + nl())
sb_push(b, "</script>" + nl())
sb_push(b, "</body>" + nl())
sb_push(b, "</html>" + nl())
return sb_str(b)
end
# HTTP response wrapper for serving pages via the tcp_* hosts
make a function called http_ok takes content_type, body returns out
let sep = chr(13) + chr(10)
let b = sb_new()
sb_push(b, "HTTP/1.1 200 OK" + sep)
sb_push(b, "Content-Type: " + content_type + "; charset=utf-8" + sep)
sb_push(b, "Content-Length: " + body.length + sep)
sb_push(b, "Connection: close" + sep)
sb_push(b, sep)
sb_push(b, body)
return sb_str(b)
end
# GUI demo in the Stage 1 self-hosted language.
# PatLang computes data (fibonacci table), then generates a well-formed
# interactive HTML+JS page: the browser is the cross-platform GUI.
# Compiled natively (or to WASM) via the PatLang front-end; writes
# gui_demo.html next to the working directory.
make a function called fib takes n returns r
if n < 2 then
return n
else
return fib(n - 1) + fib(n - 2)
end
end
make a function called fib_json takes count returns out
let b = sb_new()
sb_push(b, "[")
let i = 0
while i < count do
if i > 0 then
sb_push(b, ", ")
end
sb_push(b, "" + fib(i))
let i = i + 1
end
sb_push(b, "]")
return sb_str(b)
end
make a function called build_body returns out
let b = sb_new()
sb_push(b, "<h1>PatLang → Browser GUI</h1>" + nl())
sb_push(b, "<p>The data below was computed by native PatLang code at build time; the interactivity is generated JavaScript.</p>" + nl())
sb_push(b, "<div class='card'>" + nl())
sb_push(b, "<label>Show values above: <input type='number' id='threshold' value='0' min='0'></label>" + nl())
sb_push(b, "<button id='refresh'>Filter</button>" + nl())
sb_push(b, "<ul id='out'></ul>" + nl())
sb_push(b, "</div>" + nl())
return sb_str(b)
end
make a function called build_script takes data returns out
let b = sb_new()
sb_push(b, "const fib = " + data + ";" + nl())
sb_push(b, "function render() {" + nl())
sb_push(b, " const min = Number(document.getElementById('threshold').value) || 0;" + nl())
sb_push(b, " const out = document.getElementById('out');" + nl())
sb_push(b, " out.innerHTML = '';" + nl())
sb_push(b, " fib.forEach((v, i) => {" + nl())
sb_push(b, " if (v >= min) {" + nl())
sb_push(b, " const li = document.createElement('li');" + nl())
sb_push(b, " li.textContent = 'fib(' + i + ') = ' + v;" + nl())
sb_push(b, " out.appendChild(li);" + nl())
sb_push(b, " }" + nl())
sb_push(b, " });" + nl())
sb_push(b, "}" + nl())
sb_push(b, "document.getElementById('refresh').addEventListener('click', render);" + nl())
sb_push(b, "document.getElementById('threshold').addEventListener('input', render);" + nl())
sb_push(b, "render();" + nl())
return sb_str(b)
end
let data = fib_json(16)
let page = html_page("PatLang GUI Demo", build_body(), build_script(data))
write_file("gui_demo.html", page)
print("WROTE: gui_demo.html (" + page.length + " chars)")
Native run on the build machine (writes a real file instead of rendering into an iframe):
WROTE: gui_demo.html (1880 chars)