26 KiB
Splash DSL Guide
General Makepad Splash DSL patterns that apply to ANY app body.
Key Rules
let/fndeclarations 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: Fillis fine. - Root container MUST use
width: Fill— never a fixed pixel width. The app renders inside a parent container that provides the width. uiobject is built-in; do NOT declare it with:=forloops render widgets at build time only — array changes do NOT re-render. Useset_text()for dynamic content.- Functions with
forloops return empty strings when called fromon_click— inline string building instead as inttype casting produces NaN — use string display +set_text()only- Colons inside string arguments work correctly —
"Time: 2:30"is fine - Every
TextInputmust have a fixed numeric height (e.g.34) on_renderworks for dynamic lists. Store results in an array, render rows inside aScrollYView{on_render: || {...}}, and callui.<view>.render()after the array changes. Give the scroll view a fixedheight(e.g.height: 360) — aFillheight inside aFitparent collapses to zero.- A
:=id is only a field of its DIRECT parent. InCard{ a := Image{} inner := View{ b := Label{} } }, reach the label asCard{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
runsplashblocks run in a sandbox withnet.http_request,http_resource(...),parse_json(), andurl_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 usebase += {field: val}or a named prototypeProto{...}, not bare{...}{...}. (Solet 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:
// ✅
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:
#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:
RoundedView{draw_bg.color:#f5f5f5 height:Fit
Label{text:"Visible!" draw_text.color:#x222}
}
Label styling shorthand: Both syntaxes work:
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):
// ✅ 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 + 1correctly produces1, 2, 3, 4across consecutive clicks - Toggle:
let toggled = false; toggled = !toggledpersiststruestate 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:
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}:
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:
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: <child-name>.<property>: <value> — 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:
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
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
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
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().
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:
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:
label := Label{text:"default"} // ✅ addressable via ui.label, overridable
label: Label{text:"default"} // ❌ static — NOT addressable
Every path segment in an override must use :=:
// ✅ 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.<Name>.
⛔ 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 plainView/RoundedViewwith 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_clickworks). Prefer these for every button.glass.Button/glass.Chipare flatter, non-refracting fallbacks — only use them if you specifically want a flat chip.- Toggles →
glass.GlassRadio(a liquid-glass switch,on_clickworks).- 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/Labelon 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: truewith an opaquedraw_bg.coloron your root or on any container. Nodraw_bg.color: #x05070e, no opaqueRoundedView, no full-screen background layer, noSvgbackdrop. 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. PlainView/RoundedViewwith a fill will hide it. - The root is just a transparent layout
View(flow: Down, spacing, padding). Nothing else.
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:
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 |
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
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.<id>.set_text(...)/ui.<id>.text()calls inline inside the closure. - ❌ Do NOT call a helper
fnthat itself referencesui— it will fail with "variable ui not found". - Shared state lives in top-level
letvariables (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 withacc = acc*10 + digit. Display with"" + number. Never useas int(produces NaN). - The widget you update with
set_textmust be reachable asui.<id>— give it a:=id and keep it directly addressable.
Working glass calculator (numeric, inline ui, copy this shape)
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_bgon 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 useglass.GlassRadio. The flatglass.Button/glass.Chipdon'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
enext to a digit still need the#xprefix (e.g.#x05070e).