575 lines
26 KiB
Markdown
575 lines
26 KiB
Markdown
# 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.<view>.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: `<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:
|
||
|
||
```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.<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 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.<id>.set_text(...)` / `ui.<id>.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.<id>` — 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`).
|