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, "&amp;")
    else
      if c == 60 then
        sb_push(b, "&lt;")
      else
        if c == 62 then
          sb_push(b, "&gt;")
        else
          if c == 34 then
            sb_push(b, "&quot;")
          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 &rarr; 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)