# Splash DSL Guide General Makepad Splash DSL patterns that apply to ANY app body. ## Key Rules - **`let`/`fn` declarations must be at the top**, before any widget. The body starts with declarations, then the root widget. - **Every container MUST have `height: Fit`** — most common failure mode. Inside a fixed-height parent, `height: Fill` is fine. - **Root container MUST use `width: Fill`** — never a fixed pixel width. The app renders inside a parent container that provides the width. - `ui` object is built-in; do NOT declare it with `:=` - **`for` loops render widgets at build time only** — array changes do NOT re-render. Use `set_text()` for dynamic content. - **Functions with `for` loops return empty strings** when called from `on_click` — inline string building instead - **`as int` type casting produces NaN** — use string display + `set_text()` only - **Colons inside string arguments work correctly** — `"Time: 2:30"` is fine - Every `TextInput` must have a fixed numeric height (e.g. `34`) - `on_render` works for dynamic lists. Store results in an array, render rows inside a `ScrollYView{on_render: || {...}}`, and call `ui..render()` after the array changes. Give the scroll view a fixed `height` (e.g. `height: 360`) — a `Fill` height inside a `Fit` parent collapses to zero. - **A `:=` id is only a field of its DIRECT parent.** In `Card{ a := Image{} inner := View{ b := Label{} } }`, reach the label as `Card{inner.b.text: "x"}` — `Card{b.text: ...}` fails with "field b not found in type-check". Name every wrapper you need to address through. - AI Chat `runsplash` blocks run in a sandbox with `net.http_request`, `http_resource(...)`, `parse_json()`, and `url_encode()` enabled. Use these for HTTP-backed mini apps. - **Array / argument / object-body items may be separated by whitespace, newlines, OR commas — all work.** Adjacent values like `{a:1} {b:2}` (or one per line) are TWO separate items. Two object literals next to each other are NEVER "object inherits from object" — to extend/merge an object use `base += {field: val}` or a named prototype `Proto{...}`, not bare `{...}{...}`. (So `let days = [{...} {...} {...}]` correctly yields a 3-element array; a stray earlier bug collapsed comma-less object arrays to length 1.) ## Widget Availability **Available:** View, RoundedView, SolidView, ScrollYView, Label, TextInput, LinkLabel, Button, ButtonFlat, ButtonFlatter, Slider, CheckBox, CheckBoxFlat, RadioButton, RadioButtonFlat, ToggleFlat, DropDown, TabBar, Tab, PopupMenu, ScrollBar, ScrollBars, LoadingSpinner, Hr, Vr, Icon, Image **NOT available (silently fail):** Stack, Divider, ProgressBar, IconButton, ToggleButton, ListView, Grid, ColorPicker, ScrollPair | Wanted | Not Available | Use Instead | |--------|--------------|-------------| | Divider line | `Divider` | `Hr{height:1 width:Fill}` | | Progress bar | `ProgressBar` | `Slider{value:0.65 is_read_only:true}` | | Tabbed UI | `TabBar`/`Tab` | `ButtonFlat` rows (TabBar renders zero-size) | ## Styling Gotchas **`draw_bg.border_radius` takes a float, not an Inset:** ```splash // ✅ draw_bg.border_radius: 16.0 // ❌ parse error — silently breaks layout draw_bg.border_radius: Inset{top:0 bottom:16 left:0 right:0} ``` **`#x` prefix for hex colors containing 'e':** When a hex color contains the letter `e` adjacent to digits (like `#1e1e2e`), use `#x` to avoid parser ambiguity. Without `#x`, Makepad's parser may misinterpret digits following 'e' as an exponent: ```splash #x2ecc71 // ✅ contains 'e' next to digits, use #x #x1e1e2e // ✅ contains 'e' next to digits, use #x #ff4444 // ✅ no 'e' issue, plain # works #00ff00 // ✅ no 'e' issue ``` **Default text color is white:** All text widgets (`Label`, `Button`, etc.) default to `#fff`. For light/white backgrounds, you MUST explicitly set `draw_text.color` to a dark color on every text element: ```splash RoundedView{draw_bg.color:#f5f5f5 height:Fit Label{text:"Visible!" draw_text.color:#x222} } ``` **Label styling shorthand:** Both syntaxes work: ```splash Label{text:"Hello" color:#x2ecc71 font_size:16} // bare props work Label{text:"Hello" draw_text.color:#x2ecc71 draw_text.text_style.font_size:16} // draw_text also works ``` **`new_batch: true` for text visibility:** Required on any container with `show_bg: true` that contains text children. Without it, text renders behind the background (invisible): ```splash // ✅ Correct RoundedView{width:Fill height:Fit new_batch:true show_bg:true draw_bg.color:#x334 Label{text:"Visible" draw_text.color:#fff} } // ❌ Text may be invisible (draws behind bg) RoundedView{width:Fill height:Fit show_bg:true draw_bg.color:#x334 Label{text:"Invisible!" draw_text.color:#fff} } ``` ## Widget Reliability Reference | Widget | Capabilities | Best For | |--------|-------------|----------| | **`ButtonFlat`** | Click → variable write, `set_text()`, `text()` | All interactive controls | | **`Button`** | Click → variable write, `set_text()`, `text()` | Standard buttons | | **`Label`** | `set_text()` updates visible text, `text()` reads back | Display values, status, dynamic list display | | **`TextInput`** | `type_text` fills first input, `text()` reads value, `set_text()` writes | Text entry | | **`Image`** | `src: http_resource(url)`, `set_src(http_resource(url))`, `fit: ImageFit.CropToFill/Smallest` | Thumbnails and remote images | | **`Hr`** | Full-width line divider | Visual separation | | **`RoundedView`** | Container with rounded corners | App root, groups | ## Splash VM Variable Scope **`let` variables DO persist** across click events in the same app session: - Counter: `let count = 0; count = count + 1` correctly produces `1, 2, 3, 4` across consecutive clicks - Toggle: `let toggled = false; toggled = !toggled` persists `true` state across separate button clicks However, **widget `checked` state** on `RadioButton`, `ToggleFlat`, `CheckBox` does NOT persist because internal post-processing discards the `on_click` scope context. | Widget | Visual State | Variable Persistence | |--------|-------------|---------------------| | **`RadioButton`** | `checked: true` in widget tree | ❌ Lost — internal post-processing discards `on_click` scope | | **`ToggleFlat`** | `checked` visual renders | ❌ Same limitation | | **`CheckBox`** / **`CheckBoxFlat`** | `checked: true` in widget tree | ❌ Same limitation | **Use `ButtonFlat` with manual toggle for persistent boolean state:** ```splash let toggled = false ButtonFlat{text:"Toggle" on_click:||{toggled = !toggled; ui.display.set_text("" + toggled)}} ButtonFlat{text:"Show" on_click:||{ui.display.set_text("Current: " + toggled)}} ``` ## Patterns ### Struct Arrays & Array Operations The Splash VM supports arrays of structs with `.push()`, `.remove()`, `.len()`, and `.retain()`. Read fields via `array[index].field`, update with `array[index] += {field: val}`: ```splash let items = [ {text: "Task 1" tag: "work" done: false} {text: "Task 2" tag: "personal" done: false} ] let max_items = 5 fn add_item(text){ let clean = ("" + text).trim() if clean == "" { return } if items.len() >= max_items { return } items.push({text: clean tag: "" done: false}) sync_all() } fn toggle_item(index){ if index >= items.len() { return } items[index] += {done: !items[index].done} sync_all() } fn remove_item(index){ if index >= items.len() { return } items.remove(index) sync_all() } fn clear_flagged(){ items.retain(|it| !it.done) sync_all() } ``` ### Component / Template Pattern Define reusable templates with `let` and instantiate with property overrides: ```splash let ItemRow = RoundedView{ width: Fill height: Fit padding: Inset{top: 8 bottom: 8 left: 12 right: 12} flow: Right spacing: 10 align: Align{y: 0.5} new_batch: true draw_bg.color: #x2a2a3a draw_bg.border_radius: 8.0 label := Label{text: "item" width: Fill draw_text.color: #xddd} action := ButtonFlatter{text: "Do" width: 56 height: 28} remove := ButtonFlatter{text: "X" width: 56 height: 28} } row_0 := ItemRow{ label.text: "First item" action.on_click: || do_something(0) remove.on_click: || remove_item(0) } ``` Override syntax: `.: ` — every segment in the path must use `:=`. ### Pre-allocated Fixed Slots `for` loops render at build-time only — array changes don't add/remove widgets. Pre-allocate a fixed number of rows and update via sync functions: ```splash let items = [{text: "Item 1"} {text: "Item 2"}] fn sync_row_0(){ if 0 < items.len() { ui.row_0.label.set_text(items[0].text) } else { ui.row_0.label.set_text("Empty slot") } } fn sync_rows(){ sync_row_0() sync_row_1() sync_status() } ``` Pre-allocate 5 rows for a 5-item max list. Call `sync_rows()` after every mutation. ### Numeric State Pattern ```splash let count = 0 RoundedView{width:Fill height:Fit flow:Down spacing:10 padding:16 new_batch:true display := Label{text:"0" draw_text.color:#x44cc88 draw_text.text_style.font_size:32} View{flow:Right spacing:12 align:Align{x:0.5 y:0.5} ButtonFlat{text:"-" on_click:||{count -= 1; ui.display.set_text(count + "")}} ButtonFlat{text:"Reset" on_click:||{count = 0; ui.display.set_text("0")}} ButtonFlat{text:"+" on_click:||{count += 1; ui.display.set_text(count + "")}} } } ``` Use `count + ""` to convert numbers to strings. ### Dynamic List Display ```splash let task_count = 0 inp := TextInput{height:34} lst := Label{text:"" font_size:14.0} ButtonFlat{text:"Add" on_click:||{ let t = ui.inp.text() if t != "" { task_count = task_count + 1 let cur = ui.lst.text() if cur == " " { cur = "" } if cur != "" { cur = cur + "\n" } ui.lst.set_text(cur + task_count + ". " + t) ui.inp.set_text("") } }} ``` ### TextInput with on_return ```splash input := TextInput{ width: Fill height: 34 empty_text: "Enter something" on_return: |text| add_item(text) } Button{text: "Add" width: 64 height: 34 on_click: || add_item(ui.input.text())} ``` ### Networked Image Search For image-search mini apps in AI Chat, use the built-in network sandbox. Fetch text/JSON with `net.http_request`, parse with `.parse_json()`, and display thumbnails with `Image{src: http_resource(url)}`. Render changing result arrays through `ScrollYView.on_render` and refresh with `ui.results.render()`. ```splash let image_results = [] let ResultCard = glass.Card{ width: Fill height: Fit flow: Right spacing: 10 padding: 10 thumb := Image{width: 92 height: 72 fit: ImageFit.CropToFill} // Name every wrapper you need to reach through: a `:=` id is only a field of its // DIRECT parent. Under an unnamed `View{...}`, `title`/`source` would be unreachable // from `ResultCard{...}` ("field title not found in type-check"). info := View{width: Fill height: Fit flow: Down spacing: 3 title := glass.Body{text: "" width: Fill} source := glass.Caption{text: ""} } } fn fetch(url, extra_headers){ let p = promise() let h = {"User-Agent": "Mozilla/5.0"} if extra_headers != nil { for k, v in extra_headers { h[k] = v } } net.http_request(net.HttpRequest{url: url method: net.HttpMethod.GET headers: h}) do net.HttpEvents{ on_response: |res| p.resolve(res) on_error: |_err| p.resolve(nil) } p } fn search_images(query){ let clean = ("" + query).trim() if clean == "" { return } ui.status.set_text("Searching...") let q = clean.url_encode() let page = fetch("https://duckduckgo.com/?q=" + q + "&iax=images&ia=images", nil).await() if page == nil { ui.status.set_text("Search failed"); return } let vqd = "" let parts = page.body.to_string().split("vqd=\"") if parts.len() > 1 { vqd = parts[1].split("\"")[0] } if vqd == "" { ui.status.set_text("No image token"); return } let res = fetch("https://duckduckgo.com/i.js?l=us-en&o=json&q=" + q + "&vqd=" + vqd + "&f=,,,,,&p=1", {"Referer": "https://duckduckgo.com/"}).await() if res == nil { ui.status.set_text("No response"); return } let data = res.body.to_string().parse_json() image_results.clear() if data != nil && data.results != nil { for img in data.results { image_results.push({title: img.title source: img.source thumbnail: img.thumbnail}) } } ui.status.set_text("" + image_results.len() + " images") ui.results.render() } View{width: Fill height: Fit flow: Down spacing: 12 padding: 16 glass.H1{text: "Picture Search"} glass.Card{width: Fill height: Fit flow: Right spacing: 8 query := glass.TextInput{width: Fill height: 38 empty_text: "Search images..." on_return: |text| search_images(text)} glass.GlassButtonProminent{text: "Search" height: 38 on_click: || search_images(ui.query.text())} } status := glass.Caption{text: "Type a query"} results := ScrollYView{width: Fill height: 360 flow: Down spacing: 8 on_render: || { if image_results.len() == 0 { glass.Card{glass.Body{text: "Results will appear here."}} } else for item in image_results { ResultCard{thumb.src: http_resource(item.thumbnail) info.title.text: item.title info.source.text: item.source} } }} } ``` ### Sequential Digit Input Perform arithmetic by accumulating digits: ```splash let a = 0 ButtonFlat{text:"7" on_click:||{a = a*10+7; ui.display.set_text("" + a)}} ``` ## Naming Children: `:=` vs `:` Use `:=` for addressable children, `:` for static children: ```splash label := Label{text:"default"} // ✅ addressable via ui.label, overridable label: Label{text:"default"} // ❌ static — NOT addressable ``` Every path segment in an override must use `:=`: ```splash // ✅ Correct let Item = View{flow:Right texts := View{flow:Down label := Label{text:"default"} } } Item{texts.label.text:"new text"} // works! // ❌ Wrong — anonymous parent blocks override let Item = View{flow:Right View{flow:Down label := Label{text:"default"} // UNREACHABLE } } Item{label.text:"new text"} // silent failure ``` ## Styling Reference | Property | Example | Effect | |----------|---------|--------| | `draw_bg.color` | `#x1e1e2e` | Background color (hex) | | `draw_bg.border_radius` | `10.0` | Rounded corners | | `draw_text.color` | `#xddd` | Text color | | `draw_text.text_style.font_size` | `14` | Font size (float) | | `padding` | `Inset{top:8 bottom:8 left:12 right:12}` | Inner padding | | `spacing` | `10` | Gap between children in flow | | `align` | `Align{x:0.5 y:0.5}` | Center alignment | | `new_batch` | `true` | Required for text visibility on `show_bg:true` containers | | `empty_text` | `"Type here..."` | Placeholder for TextInput | ## Not in Build | Widget | Behavior | |--------|----------| | **`TabBar`** / **`Tab`** | width=0, height=0 — no visible output | --- # Liquid Glass widgets (`glass.*`) Makepad ships an Apple-style "Liquid Glass" widget kit under the `glass.` namespace. It is already in scope inside every `runsplash` block (no import needed). All names below are addressed as `glass.`. > ## ⛔ HARD REQUIREMENT — use glass widgets > The chat this renders in is a **glass-styled app**. Unless the user explicitly asks for a plain/non-glass look, you **MUST** build every app out of `glass.*` widgets on a detailed backdrop: > - Surfaces/cards → `glass.Card` / `glass.Panel` / `glass.Group` (NOT plain `View`/`RoundedView` with a flat fill). > - **Buttons → `glass.GlassButton` / `glass.GlassButtonProminent`** — these are the REAL liquid-glass buttons that lens and refract the backdrop (the proper "gauss glass" look). They are fully clickable (`on_click` works). **Prefer these for every button.** `glass.Button` / `glass.Chip` are flatter, non-refracting fallbacks — only use them if you specifically want a flat chip. > - Toggles → `glass.GlassRadio` (a liquid-glass switch, `on_click` works). > - Text → `glass.H1` / `glass.H2` / `glass.Body` / `glass.Caption` / `glass.OptionLabel`. > - Inputs/toggles/sliders → `glass.TextInput` / `glass.SearchField` / `glass.Toggle` / `glass.Slider` / `glass.GlassRadio`. > - And the app MUST open with a **detailed colourful backdrop** (see the golden rule below) so the glass is actually visible. > > An app that uses plain `Button`/`View`/`Label` on a flat dark fill is WRONG for this chat — it will look like a generic dark app with no glass. ## THE GOLDEN RULE: be TRANSPARENT — the chat already paints the backdrop The chat this renders in **already paints a rich, detailed, colourful backdrop** behind your app. Your job is to float **transparent glass** on top of it so the glass refracts that backdrop. Glass lenses whatever is painted behind it; if you paint an opaque background, the glass has nothing to bend and the whole thing looks like a flat dark app. **Therefore your app MUST be transparent:** - **NEVER** put `show_bg: true` with an opaque `draw_bg.color` on your root or on any container. No `draw_bg.color: #x05070e`, no opaque `RoundedView`, no full-screen background layer, no `Svg` backdrop. The host backdrop is already there — let it show through. - Build surfaces **only** from `glass.*` widgets (`glass.Card`, `glass.Panel`, `glass.Group`) — these are already translucent and refract the backdrop. Plain `View`/`RoundedView` with a fill will hide it. - The root is just a transparent layout `View` (`flow: Down`, spacing, padding). Nothing else. ```splash View{ width: Fill height: Fit flow: Down spacing: 14 padding: 20 // No background here — transparent on purpose so the chat backdrop shows through. glass.H1{text: "Glass Kit"} glass.Body{text: "A liquid-glass showcase."} glass.Card{ glass.H2{text: "Liquid Glass"} glass.Body{text: "Floats above the chat backdrop, refracting it."} } View{width:Fill height:Fit flow:Right spacing:10 glass.GlassButtonProminent{text:"Continue"} glass.GlassButton{text:"Cancel"} } } ``` If you catch yourself writing `draw_bg.color` on a container, delete it and use a `glass.Card`/`glass.Panel` instead. ## Surfaces / containers | Widget | Use for | Notes | |--------|---------|-------| | `glass.Panel` | Main glass card / sheet | Fill width, Fit height, flow Down, padding 16. Also aliased `GlassPanel`. | | `glass.Card` | Content card | More tint than Panel — good default card. | | `glass.Group` | Grouped controls | Tighter padding than Panel. | | `glass.ClearPanel` | Clearer floating surface | Stronger lensing, less tint. | | `glass.NavBar` | Top/bottom bar | height 58, flow Right. | | `glass.TabBar` | Floating tab bar | height 66, flow Right. Put `glass.Caption`/`Label` children in it. (This is the glass TabBar — NOT the broken core `TabBar`.) | | `glass.List` | List container | translucent panel, flow Down. | | `glass.ListRow` | One row in a list | height 54, flow Right, vertically centered. | | `glass.Badge` | Small status pill | green-tinted capsule. | All surfaces already set `width`/`height`/`flow`/`padding`, so you can drop children straight in. They do NOT need `new_batch` (they manage their own batching), but the **outer dark root DOES** (`new_batch: true` + `show_bg: true`). Every glass surface already carries a **built-in soft drop shadow** (kept deliberately subtle by default). Do not add your own large shadows, and don't crank `shadow_radius`/`shadow_offset` — an oversized shadow reads as "fat" and gets clipped by tight parent bounds. If you ever need to tune it, keep it small (e.g. `draw_bg +: { shadow_radius: 9.0 shadow_offset: vec2(0.0, 3.0) }`). ## Buttons (reliable — use these for clicks) `glass.Button`, `glass.ProminentButton`, `glass.IconButton`, and `glass.Chip` are flat SDF glass buttons built on `ButtonFlat`. They are the **reliable interactive choice** and support `on_click`, `set_text()`, `text()` exactly like `Button`: ```splash Row{flow:Right spacing:10 glass.GlassButtonProminent{text:"Add" on_click:|| add_item(ui.inp.text())} glass.GlassButton{text:"Clear" on_click:|| clear_all()} glass.Chip{text:"All"} } ``` ## Premium lensing controls (decorative-first) These are custom widgets that draw a real refracting lens over the scene. They look spectacular but are **best used as showcase / display controls** — prefer the flat `glass.Button` family when you need a reliable `on_click` handler in a generated app. | Widget | What it is | Default size | |--------|-----------|--------------| | `glass.GlassButton` | Refracting pill button with a label | Fit × 44, has `text:` | | `glass.GlassButtonProminent` | Blue-tinted refracting button | same | | `glass.GlassRadio` | Liquid toggle switch (slides + "gloops") | 70 × 34 | | `glass.GlassSlider` | Refracting slider knob | Fill × 32, `value: 0.4` | ```splash ToggleRow{flow:Right spacing:16 align:Align{y:0.5} glass.GlassRadio{} glass.OptionLabel{text:"Wi-Fi"} } glass.GlassButton{text:"Continue"} ``` > Lensing widgets sample a blurred snapshot of the window. Inside the chat they fall back to a solid frosted color when no snapshot is available, so they still render — just less dramatically than in a full-screen app like `example-glass`. ## Inputs | Widget | Use for | |--------|---------| | `glass.TextInput` | Glass text field (height 38) | | `glass.SearchField` | Same, "Search" placeholder | | `glass.Slider` | Labeled glass slider (height 40) | | `glass.Toggle` | Glass on/off toggle | | `glass.RadioButton` | Glass pill radio | ## Typography (pre-styled white-on-dark labels) Use these instead of restyling `Label` by hand — they already set the right color and weight for dark glass UIs: `glass.H1` (30), `glass.H2` (18), `glass.OptionLabel` (16), `glass.Body` (13, Fill width), `glass.ButtonLabel` (13), `glass.Caption` (11, muted blue — good for SECTION HEADERS). ## Full minimal glass app skeleton ```splash fn noop(){} RoundedView{ width: Fill height: Fit flow: Down spacing: 14 padding: 20 new_batch: true show_bg: true draw_bg.color: #x05070e draw_bg.border_radius: 18.0 glass.H1{text: "Settings"} glass.Caption{text: "NETWORK"} glass.Card{ View{width:Fill height:Fit flow:Right spacing:16 align:Align{y:0.5} glass.GlassRadio{} glass.OptionLabel{text:"Wi-Fi"} } } glass.Caption{text: "ACTIONS"} Row{flow:Right spacing:10 glass.GlassButtonProminent{text:"Save" on_click:|| noop()} glass.GlassButton{text:"Cancel" on_click:|| noop()} } } ``` ## Interactivity that ACTUALLY works (read this for calculators / counters / forms) The `ui` object is only injected into the scope of an **`on_click` / `on_return` / `on_change` closure**. It is **NOT visible inside a separately-declared `fn`**. So: - ✅ Do all `ui..set_text(...)` / `ui..text()` calls **inline inside the closure**. - ❌ Do NOT call a helper `fn` that itself references `ui` — it will fail with "variable ui not found". - Shared state lives in top-level `let` variables (these persist across clicks). Mutate them inline. - Arithmetic must stay **numeric** — never store a number as a string and add it (string `+` concatenates: `"5"+"3"=="53"`). Build numbers with `acc = acc*10 + digit`. Display with `"" + number`. Never use `as int` (produces NaN). - The widget you update with `set_text` must be reachable as `ui.` — give it a `:=` id and keep it directly addressable. ### Working glass calculator (numeric, inline `ui`, copy this shape) ```splash let acc = 0.0 // number currently being entered let stored = 0.0 // left-hand operand let op = "" View{ width: Fill height: Fit flow: Down spacing: 12 padding: 18 // Transparent root — the chat backdrop shows through and the glass refracts it. glass.Card{ display := glass.H1{text:"0" width: Fill align: Align{x:1.0}} } View{width:Fill height:Fit flow:Right spacing:8 glass.GlassButton{text:"7" width:Fill height:52 on_click:|| { acc = acc*10.0 + 7.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"8" width:Fill height:52 on_click:|| { acc = acc*10.0 + 8.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"9" width:Fill height:52 on_click:|| { acc = acc*10.0 + 9.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"+" width:Fill height:52 on_click:|| { stored = acc; acc = 0.0; op = "+" }} } View{width:Fill height:Fit flow:Right spacing:8 glass.GlassButton{text:"4" width:Fill height:52 on_click:|| { acc = acc*10.0 + 4.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"5" width:Fill height:52 on_click:|| { acc = acc*10.0 + 5.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"6" width:Fill height:52 on_click:|| { acc = acc*10.0 + 6.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"-" width:Fill height:52 on_click:|| { stored = acc; acc = 0.0; op = "-" }} } View{width:Fill height:Fit flow:Right spacing:8 glass.GlassButton{text:"1" width:Fill height:52 on_click:|| { acc = acc*10.0 + 1.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"2" width:Fill height:52 on_click:|| { acc = acc*10.0 + 2.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"3" width:Fill height:52 on_click:|| { acc = acc*10.0 + 3.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"*" width:Fill height:52 on_click:|| { stored = acc; acc = 0.0; op = "*" }} } View{width:Fill height:Fit flow:Right spacing:8 glass.GlassButton{text:"0" width:Fill height:52 on_click:|| { acc = acc*10.0; ui.display.set_text("" + acc) }} glass.GlassButton{text:"C" width:Fill height:52 on_click:|| { acc = 0.0; stored = 0.0; op = ""; ui.display.set_text("0") }} glass.GlassButtonProminent{text:"=" width:Fill height:52 on_click:|| { let r = stored if op == "+" { r = stored + acc } if op == "-" { r = stored - acc } if op == "*" { r = stored * acc } acc = r; op = "" ui.display.set_text("" + r) }} } } ``` Every digit/operator handler is **inline** (no helper `fn`), so `ui.display` is in scope, and all arithmetic is numeric. This pattern computes correctly AND looks glassy. ## Glass gotchas - **Transparent root is mandatory** — the chat paints the backdrop; if you paint your own opaque background the glass refracts nothing and looks like a flat dark app. No `draw_bg.color` / `show_bg` on your root or containers. - For buttons use the **lensing** `glass.GlassButton` / `glass.GlassButtonProminent` (they refract the backdrop — the real gauss-glass look — and are fully clickable). For toggles use `glass.GlassRadio`. The flat `glass.Button`/`glass.Chip` don't refract; only use them when you explicitly want a flat chip. - Don't restyle glass surface colors — they're tuned. Just set layout (`width`/`height`/`spacing`/`padding`) and drop content in. - Hex colors with `e` next to a digit still need the `#x` prefix (e.g. `#x05070e`).