Splash is Makepad's UI scripting language. No commas between properties. No semicolons. Whitespace-delimited.
Do NOT use Root{} or Window{} — those are host-level wrappers handled externally. Your output is the content inside a body/splash widget.
In Splash, when you declare a named child widget inside a let template (or any container), you use the := operator. This marks the child as a named/dynamic property — addressable and overridable per-instance.
- To declare:
label := Label{text: "default"} - To override:
MyTemplate{label.text: "new value"}
If you write label: (colon) instead of label := (colon-equals), the child is a static property — not addressable, and overrides fail silently (text becomes invisible).
Use := for any child you want to reference or override later: check :=, label :=, tag :=, title :=, body :=, icon :=, content :=, etc.
let TodoItem = View{
width: Fill height: Fit
padding: Inset{top: 8 bottom: 8 left: 12 right: 12}
flow: Right spacing: 10
align: Align{y: 0.5}
check := CheckBox{text: ""}
label := Label{text: "task" draw_text.color: #ddd draw_text.text_style.font_size: 11}
Filler{}
tag := Label{text: "" draw_text.color: #888 draw_text.text_style.font_size: 9}
}
RoundedView{
width: 380 height: Fit
flow: Down spacing: 4
padding: 16
new_batch: true
draw_bg.color: #1e1e2e
draw_bg.border_radius: 10.0
Label{text: "My Tasks" draw_text.color: #fff draw_text.text_style.font_size: 14}
Hr{}
TodoItem{label.text: "Buy groceries" tag.text: "errands"}
TodoItem{label.text: "Fix login bug" tag.text: "urgent"}
TodoItem{label.text: "Write unit tests" tag.text: "dev"}
TodoItem{label.text: "Call the dentist" tag.text: "personal"}
}
let InfoCard = RoundedView{
width: Fill height: Fit
padding: 16 flow: Down spacing: 6
draw_bg.color: #2a2a3d
draw_bg.border_radius: 8.0
title := Label{text: "Title" draw_text.color: #fff draw_text.text_style.font_size: 14}
body := Label{text: "Body" draw_text.color: #aaa draw_text.text_style.font_size: 11}
}
View{
flow: Down height: Fit spacing: 10 padding: 20
InfoCard{title.text: "First card" body.text: "Some content here"}
InfoCard{title.text: "Second card" body.text: "More content here"}
}
ONLY use widgets, properties, and syntax documented in this manual. This code must compile and run — do not:
- Invent new properties (e.g., don't write
background_color:— usedraw_bg.color:) - Guess at property names (e.g., don't write
font_size:— usedraw_text.text_style.font_size:) - Make up new widgets that aren't listed here
- Suggest hypothetical features or syntax that "might work"
- Use CSS-like property names (no
border-radius, usedraw_bg.border_radius)
If you're unsure whether a property exists, don't use it. Stick to the exact syntax shown in the examples.
When generating UI, output ONLY the Splash code. Do not add:
- Explanatory text before or after the code
- "Here's the UI:" or "This creates..." preambles
- Suggestions for improvements or alternatives
- Commentary about what the code does
Just output the raw Splash script starting with use mod.prelude.widgets.* — nothing else.
STOP. READ THIS. THE #1 MISTAKE IS FORGETTING height: Fit.
┌─────────────────────────────────────────────────────────────────┐
│ EVERY View, SolidView, RoundedView MUST HAVE height: Fit │
│ │
│ ✅ View{ flow: Down height: Fit padding: 10 ... } │
│ │
│ If you forget height: Fit, your UI will be INVISIBLE (0px) │
└─────────────────────────────────────────────────────────────────┘
Why? The default is height: Fill. Your output renders in a Fit container. Fill inside Fit = circular dependency = 0 height.
ALWAYS write height: Fit immediately after the opening brace:
View{ height: Fit flow: Down padding: 10
Label{text: "Visible!"}
}
SolidView{ height: Fit width: Fill draw_bg.color: #333
Label{text: "Also visible!"}
}
RoundedView{ height: Fit width: Fill flow: Down spacing: 8
Label{text: "Card content"}
}
Exceptions:
- Inside a fixed-height parent,
height: Fillis OK:
View{ height: 300 // Fixed parent
View{ height: Fill // OK here - fills the 300px
Label{text: "I fill the fixed 300px"}
}
}
- MapView — has no intrinsic height, so
height: Fitalso gives 0px. Use a fixed pixel height:MapView{width: Fill height: 500}
TEMPLATE: Copy this pattern for every container:
View{ height: Fit ...rest of properties...
...children...
}
NEVER use a fixed pixel width (e.g., width: 400) on your outermost container. Your output renders inside a container that provides available width — use width: Fill to fill it.
┌─────────────────────────────────────────────────────────────────┐
│ The ROOT container MUST use width: Fill │
│ │
│ ✅ RoundedView{ width: Fill height: Fit ... } │
│ ❌ RoundedView{ width: 400 height: Fit ... } │
│ │
│ Fixed widths make your UI a narrow sliver or completely broken │
└─────────────────────────────────────────────────────────────────┘
Why? A fixed width like width: 400 does not adapt to the available space. Worse, if the parent container is narrower than 400, your content gets clipped. If a parse error occurs anywhere in the code, the entire layout can collapse to near-zero width.
ALWAYS use width: Fill on the root element:
RoundedView{ width: Fill height: Fit flow: Down
// your content
}
Fixed pixel widths are fine for inner elements like icons, avatars, or specific components — just never on the outermost container.
✅ draw_bg.border_radius: 16.0
❌ draw_bg.border_radius: Inset{top: 0 bottom: 16 left: 0 right: 0}
border_radius is a single f32 value applied uniformly to all corners. Passing an Inset or object will cause a parse error that can silently break your entire layout.
Do NOT use View{ show_bg: true ... } — the raw View has an ugly green test color as its background.
Instead, use these pre-styled container widgets that have proper backgrounds:
| Widget | Use for |
|---|---|
SolidView |
Simple solid color background |
RoundedView |
Rounded corners with optional border |
RectView |
Rectangle with optional border |
RoundedShadowView |
Rounded corners with drop shadow |
RectShadowView |
Rectangle with drop shadow |
CircleView |
Circular shape |
GradientXView |
Horizontal gradient |
GradientYView |
Vertical gradient |
All have show_bg: true already set. Set color via draw_bg.color:
SolidView{ width: Fill height: Fit draw_bg.color: #334
Label{text: "Content here"}
}
RoundedView{ width: Fill height: Fit draw_bg.color: #445 draw_bg.border_radius: 8.0
Label{text: "Rounded card"}
}
RoundedShadowView{ width: Fill height: Fit draw_bg.color: #556 draw_bg.shadow_radius: 10.0
Label{text: "Card with shadow"}
}
Use raw View{} only when you need no background (invisible layout container).
Every splash script must start with a use statement to bring widgets into scope:
use mod.prelude.widgets.*
// Now all widgets (View, Label, Button, etc.) are available
View{
flow: Down
height: Fit // ← ALWAYS set height! Default is Fill which breaks in Fit containers
padding: 20
Label{text: "Hello world"}
}
Without use mod.prelude.widgets.* at the top, widget names like View, Label, Button etc. will not be found.
Use let to define reusable widget templates. let bindings must be defined ABOVE (before) the places where they are used. They are local to the current scope.
When a template has children you want to customize per-instance, you MUST use id := declarations. See the critical rule below.
use mod.prelude.widgets.*
// Simple template with NO per-instance children — just style overrides
let MyHeader = Label{
draw_text.color: #fff
draw_text.text_style.font_size: 16
}
// Template WITH per-instance children — MUST use id := declarations
let MyCard = RoundedView{
width: Fill height: Fit
padding: 15 flow: Down spacing: 8
draw_bg.color: #334
draw_bg.border_radius: 8.0
title := Label{text: "default" draw_text.color: #fff draw_text.text_style.font_size: 16}
body := Label{text: "" draw_text.color: #aaa}
}
// Override children using id.property syntax
View{
flow: Down height: Fit
spacing: 12 padding: 20
MyCard{title.text: "First Card" body.text: "Content here"}
MyCard{title.text: "Second Card" body.text: "More content"}
}
Children inside a let template that you want to override per-instance MUST be declared with :=. This is part of the syntax — label := creates a named/dynamic child, label: does not.
Reusable todo/list item with multiple named children:
let TodoItem = View{
width: Fill height: Fit
padding: Inset{top: 8 bottom: 8 left: 12 right: 12}
flow: Right spacing: 8
align: Align{y: 0.5}
check := CheckBox{text: ""}
label := Label{text: "task" draw_text.color: #ddd draw_text.text_style.font_size: 11}
Filler{}
tag := Label{text: "" draw_text.color: #888 draw_text.text_style.font_size: 9}
}
View{
flow: Down height: Fit spacing: 4
TodoItem{label.text: "Walk the dog" tag.text: "personal"}
TodoItem{label.text: "Fix login bug" tag.text: "urgent"}
TodoItem{label.text: "Buy groceries" tag.text: "errands"}
}
You can override ANY property on an id := child: label.draw_text.color: #f00, icon.visible: false, subtitle.draw_text.text_style.font_size: 10, etc.
⛔ Named children inside anonymous containers are UNREACHABLE. If a := child is nested inside an anonymous View{} (no := on the View), the override path cannot find it. The override fails silently and the default text shows instead:
Every container in the path from root to the child must have a := name. Then use the full dot-path to override:
let Item = View{
flow: Right
texts := View{ // named with :=
flow: Down
label := Label{text: "default"}
}
}
Item{texts.label.text: "new text"} // full path through named containers
// Property assignment
key: value
// Nested object
key: Type{ prop1: val1 prop2: val2 }
// Merge (extend parent, don't replace)
key +: { prop: val }
// Dot-path shorthand
draw_bg.color: #f00
// equivalent to: draw_bg +: { color: #f00 }
// Named child (:= declares a dynamic/addressable child)
my_button := Button{ text: "Click" }
// Anonymous child (no name)
Label{ text: "hello" }
// Let binding (define BEFORE use, local to current scope)
let MyThing = View{ height: Fit width: Fill }
// Instantiate let binding
MyThing{}
// Inherit from existing widget type
MyView = RoundedView{ height: Fit draw_bg.color: #f00 }
#f00 // RGB short
#ff0000 // RGB full
#ff0000ff // RGBA
#0000 // transparent black
vec4(1.0 0.0 0.0 1.0) // explicit RGBA
width: Fill // Fill available space (default)
width: Fit // Shrink to content
width: 200 // Fixed 200px (bare number = Fixed)
width: Fill{min: 100 max: 500}
width: Fit{max: Abs(300)}
height: Fill height: Fit height: 100
flow: Right // default, left-to-right (no wrap)
flow: Down // top-to-bottom
flow: Overlay // stacked on top of each other
flow: Flow.Right{wrap: true} // wrapping horizontal
flow: Flow.Down{wrap: true} // wrapping vertical
spacing: 10 // gap between children
padding: 15 // uniform padding (bare number)
padding: Inset{top: 5 bottom: 5 left: 10 right: 10}
margin: Inset{top: 2 bottom: 2 left: 5 right: 5}
margin: 0. // uniform zero
align: Center // Align{x:0.5 y:0.5}
align: HCenter // Align{x:0.5 y:0.0}
align: VCenter // Align{x:0.0 y:0.5}
align: TopLeft // Align{x:0.0 y:0.0}
align: Align{x: 1.0 y: 0.0} // top-right
align: Align{x: 0.0 y: 0.5} // center-left
clip_x: true // default
clip_y: true // default
clip_x: false // overflow visible
All inherit from ViewBase. Default: no background.
| Widget | Background | Shape |
|---|---|---|
View |
none | - |
SolidView |
flat color | rectangle |
RoundedView |
color | rounded rect (border_radius) |
RoundedAllView |
color | per-corner radius (vec4) |
RoundedXView |
color | left/right radius (vec2) |
RoundedYView |
color | top/bottom radius (vec2) |
RectView |
color | rectangle with border |
RectShadowView |
color+shadow | rectangle |
RoundedShadowView |
color+shadow | rounded rect |
CircleView |
color | circle |
HexagonView |
color | hexagon |
GradientXView |
horizontal gradient | rectangle |
GradientYView |
vertical gradient | rectangle |
CachedView |
texture-cached | rectangle |
CachedRoundedView |
texture-cached | rounded rect |
ScrollXYView{} // scroll both axes
ScrollXView{} // horizontal scroll
ScrollYView{} // vertical scroll
height: Fit (default is Fill which breaks in chat output!)
// Layout (inherited by all containers)
width: Fill // Size: Fill | Fit | <number>
height: Fit // ⚠️ USE Fit! Default Fill breaks in Fit containers!
flow: Down // Flow: Right | Down | Overlay | Flow.Right{wrap:true}
spacing: 10 // gap between children
padding: 15 // Inset or bare number
margin: 0. // Inset or bare number
align: Center // Align preset or Align{x: y:}
// Display
show_bg: true // enable background drawing (false by default)
visible: true
new_batch: true // see "Draw Batching" section below
cursor: MouseCursor.Hand
grab_key_focus: true
block_signal_event: false
capture_overload: false
clip_x: true
clip_y: true
// Scrollbar (for ScrollXView/ScrollYView/ScrollXYView)
scroll_bars: ScrollBar{}
In Makepad, widgets that use the same shader are automatically collected into the same GPU draw call for performance. This means if you draw Label{} SolidView{ Label{} }, the second Label's text can end up behind the SolidView's background — because both Labels are batched into the same text draw call, which executes before the SolidView's background draw call.
Set new_batch: true on any View that has show_bg: true AND contains text children. This tells the View to start a new draw batch, ensuring its background is drawn before its children's text.
⛔ CRITICAL for hover effects: If a View has show_bg: true with a hover animator (background goes from transparent #0000 to opaque on hover), you MUST set new_batch: true on that View. Without it, when the hover activates the background becomes opaque and covers the text — making text disappear on hover. This is the #1 mistake with hoverable list items.
When to use new_batch: true:
- Any View/SolidView/RoundedView with
show_bg: truethat contains Labels or other text — always addnew_batch: true - Hoverable items — a View with
show_bg: true+ animator hover that contains text MUST havenew_batch: trueor text vanishes on hover - Container of repeated items that each have their own background — the container itself also needs
new_batch: true - When text appears invisible despite having the correct color — this is almost always a batching issue
// Hoverable item: new_batch ensures text draws on top of hover bg
let HoverItem = View{
width: Fill height: Fit
new_batch: true
show_bg: true
draw_bg +: { color: uniform(#0000) color_hover: uniform(#fff2) hover: instance(0.0) ... }
animator: Animator{ hover: { ... } }
label := Label{text: "item" draw_text.color: #fff}
}
// Parent container of repeated items also needs new_batch
RoundedView{
flow: Down height: Fit new_batch: true
HoverItem{label.text: "Walk the dog"}
HoverItem{label.text: "Do laundry"}
}
draw_bg +: {
color: instance(#334) // fill color
color_2: instance(vec4(-1)) // gradient end (-1 = disabled)
gradient_fill_horizontal: uniform(0.0) // 0=vertical, 1=horizontal
border_size: uniform(1.0)
border_radius: uniform(5.0) // for RoundedView
border_color: instance(#888)
border_inset: uniform(vec4(0))
// Shadow views add:
shadow_color: instance(#0007)
shadow_radius: uniform(10.0)
shadow_offset: uniform(vec2(0 0))
}
Properties: text, draw_text (DrawText), align, flow, padding, hover_actions_enabled
animator or cursor. Adding them has no effect — they are silently ignored. To make hoverable/clickable text, wrap a Label inside a View with animator+cursor (see Animator section for example).
Label{ text: "Hello" }
Label{
width: Fit height: Fit
draw_text.color: #fff
draw_text.text_style.font_size: 12
text: "Styled"
}
⛔ CRITICAL: Default text color is WHITE. All text widgets (Label, H1, H2, Button text, etc.) default to white (#fff). For light/white themes, you MUST explicitly set draw_text.color to a dark color on EVERY text element, or text will be invisible (white-on-white). Example:
For light themes, always set dark text explicitly:
RoundedView{ draw_bg.color: #f5f5f5 height: Fit new_batch: true
Label{text: "Visible!" draw_text.color: #222}
}
| Widget | Description |
|---|---|
Label |
Default label |
Labelbold |
Bold font |
LabelGradientX |
Horizontal text gradient |
LabelGradientY |
Vertical text gradient |
TextBox |
Full-width, long-form text_style |
P |
Paragraph (like TextBox) |
Pbold |
Bold paragraph |
H1{ text: "Title" } // font_size_1
H2{ text: "Subtitle" } // font_size_2
H3{ text: "Section" } // font_size_3
H4{ text: "Subsection" } // font_size_4
draw_text +: {
color: #fff
color_2: uniform(vec4(-1)) // gradient end (-1 = disabled)
color_dither: uniform(1.0)
gradient_fill_horizontal: uniform(0.0)
text_style: theme.font_regular{ font_size: 11 }
}
Available fonts: theme.font_regular, theme.font_bold, theme.font_italic, theme.font_bold_italic, theme.font_code, theme.font_icons
Properties: is_password, is_read_only, is_numeric_only, empty_text, draw_bg, draw_text, draw_selection, draw_cursor, label_align
TextInput{ width: Fill height: Fit empty_text: "Placeholder" }
TextInputFlat{ width: Fill height: Fit empty_text: "Type here" }
TextInput{ is_password: true empty_text: "Password" }
TextInput{ is_read_only: true }
TextInput{ is_numeric_only: true }
Properties: same as Button (text, draw_text, draw_bg, icon_walk, label_walk)
LinkLabel{ text: "Click me" }
TextFlow{
width: Fill height: Fit
selectable: true
font_size: 10
}
Markdown{
width: Fill height: Fit
selectable: true
body: "# Title\n\nParagraph with **bold**"
}
Html{
width: Fill height: Fit
body: "<h3>Title</h3><p>Content</p>"
}
Properties: text, draw_bg (DrawQuad), draw_text (DrawText), draw_icon (DrawSvg), icon_walk, label_walk, grab_key_focus, animator
Button{ text: "Standard" }
ButtonFlat{ text: "Flat" } // no bevel border
ButtonFlatter{ text: "Minimal" } // invisible bg
// With icon
Button{
text: "Save"
icon_walk: Walk{width: 16 height: 16}
draw_icon.color: #fff
draw_icon.svg: crate_resource("self://path/to/icon.svg")
}
// Customize colors
ButtonFlat{
text: "Custom"
draw_bg +: {
color: uniform(#336)
color_hover: uniform(#449)
color_down: uniform(#225)
}
draw_text +: {
color: #fff
}
}
These are per-instance floats driven by the animator:
hover, down, focus, disabled
Color uniforms (each with _hover, _down, _focus, _disabled variants):
color, color_2, border_color, border_color_2
Other: border_size, border_radius, color_dither, gradient_fill_horizontal, gradient_border_horizontal
CheckBox/Toggle share a base. Properties: text, draw_bg, draw_text, draw_icon, icon_walk, label_walk, label_align, animator
CheckBox{ text: "Enable" }
CheckBoxFlat{ text: "Flat style" }
Toggle{ text: "Dark mode" }
ToggleFlat{ text: "Flat toggle" }
CheckBoxCustom{ text: "Custom" }
Animator-driven: hover, down, focus, active, disabled
Uniforms: size, border_size, border_radius
Color uniforms (each with _hover, _down, _active, _focus, _disabled): color, border_color, mark_color
Also: mark_size
Properties: same as CheckBox
RadioButton{ text: "Option A" }
RadioButtonFlat{ text: "Option A" }
Properties: text, min, max, step, default, precision, axis (DragAxis), label_walk, label_align, draw_bg, draw_text, bind
Slider{ width: Fill text: "Volume" min: 0.0 max: 100.0 default: 50.0 }
SliderMinimal{ text: "Value" min: 0.0 max: 1.0 step: 0.01 precision: 2 }
Properties: labels (string array), draw_bg, draw_text, popup_menu, bind, bind_enum
DropDown{ labels: ["Option A" "Option B" "Option C"] }
DropDownFlat{ labels: ["Small" "Medium" "Large"] }
Properties: draw_bg (DrawImage), fit (ImageFit), min_width, min_height, width_scale, animation (ImageAnimation)
Image{ width: 200 height: 150 fit: ImageFit.Stretch }
// ImageFit: Stretch | Horizontal | Vertical | Smallest | Biggest | Size
// ImageAnimation: Stop | Once | Loop | Bounce | OnceFps(60) | LoopFps(25) | BounceFps(25)
draw_bg +: {
opacity: 1.0
image_scale: vec2(1.0 1.0)
image_pan: vec2(0.0 0.0)
image_texture: texture_2d(float)
}
Properties: draw_bg, draw_icon (DrawSvg), icon_walk
Icon{
draw_icon.svg: crate_resource("self://resources/icons/my_icon.svg")
draw_icon.color: #0ff
icon_walk: Walk{width: 32 height: 32}
}
A View with animated arc shader. Properties: color, rotation_speed, border_size, stroke_width, max_gap_ratio, min_gap_ratio
LoadingSpinner{ width: 40 height: 40 }
Hr{} // horizontal rule
Vr{} // vertical rule
Filler{} // View{width: Fill height: Fill} - pushes siblings apart
⛔ Do NOT use Filler{} next to a width: Fill sibling in flow: Right. Both compete for remaining space and split it 50/50, causing text to be clipped halfway. Instead, give the content element width: Fill — it naturally pushes width: Fit siblings to the edge. Only use Filler{} between width: Fit siblings:
// Filler between Fit siblings — correct use
View{ flow: Right
Label{text: "left"}
Filler{}
Label{text: "right"}
}
// width: Fill takes remaining space, pushes Fit siblings right — no Filler needed
View{ flow: Right
texts := View{ width: Fill height: Fit flow: Down
label := Label{text: "title"}
sub := Label{text: "subtitle"}
}
tag := Label{text: "tag"}
}
Properties: axis (SplitterAxis), align (SplitterAlign), a, b, size, min_horizontal, max_horizontal, min_vertical, max_vertical, draw_bg
Splitter{
axis: SplitterAxis.Horizontal // Horizontal | Vertical
align: SplitterAlign.FromA(250.0) // FromA(px) | FromB(px) | Weighted(0.5)
a := left_panel
b := right_panel
}
Note: a and b reference named children — use a := left_panel (the := operator) to bind them.
Properties: body_walk, animator (with active group: on/off states controlling opened float)
FoldHeader{
header: View{ height: Fit
flow: Right align: Align{y: 0.5} spacing: 8
FoldButton{}
Label{text: "Section Title"}
}
body: View{ height: Fit
flow: Down padding: Inset{left: 23} spacing: 8
// content
}
}
Properties: flow, scroll_bar, capture_overload, selectable, drag_scrolling, auto_tail
Define templates with := declarations. Templates are instantiated by host code at draw time.
list := PortalList{
width: Fill height: Fill
flow: Down
scroll_bar: ScrollBar{}
Item := View{
width: Fill height: Fit
title := Label{text: ""}
}
Header := View{ height: Fit ... }
}
FlatList{
width: Fill height: Fill
flow: Down
Item := View{ height: Fit ... }
}
Properties: bar_size, bar_side_margin, min_handle_size, draw_bg
ScrollBar{
bar_size: 10.0
bar_side_margin: 3.0
min_handle_size: 30.0
}
The Dock is a tabbed panel layout with splitters, tabs, and content templates. Three sections:
tab_bar +:— define tab header templates (appearance of tab buttons)root :=— the layout tree of DockSplitter/DockTabs- Content templates —
Name := Widget{}defines content instantiated by tabs
tab_bar (TabBar widget for tab headers), splitter (Splitter widget), round_corner, drag_target_preview, padding
axis (SplitterAxis), align (SplitterAlign), a (LiveId ref), b (LiveId ref)
tabs (array of tab refs), selected (index), closable
name (string), template (ref to tab_bar template), kind (ref to content template)
Dock{
width: Fill height: Fill
// 1. Tab header templates (how tab buttons look)
tab_bar +: {
FilesTab := IconTab{
draw_icon +: {
color: #80FFBF
svg: crate_resource("self://resources/icons/icon_file.svg")
}
}
EditTab := IconTab{
draw_icon +: {
color: #FFB368
svg: crate_resource("self://resources/icons/icon_editor.svg")
}
}
LogTab := IconTab{
draw_icon +: {
color: #80FFBF
svg: crate_resource("self://resources/icons/icon_log.svg")
}
}
}
// 2. Layout tree
root := DockSplitter{
axis: SplitterAxis.Horizontal
align: SplitterAlign.FromA(250.0)
a := left_tabs
b := right_split
}
right_split := DockSplitter{
axis: SplitterAxis.Vertical
align: SplitterAlign.FromB(200.0)
a := center_tabs
b := bottom_tabs
}
left_tabs := DockTabs{
tabs: [@files_tab]
selected: 0
}
center_tabs := DockTabs{
tabs: [@edit_tab]
selected: 0
}
bottom_tabs := DockTabs{
tabs: [@log_tab]
selected: 0
}
// 3. Tab definitions (connect header template to content template)
files_tab := DockTab{
name: "Files"
template := FilesTab // references tab_bar template
kind := FileTreeContent // references content template
}
edit_tab := DockTab{
name: "Editor"
template := EditTab
kind := EditorContent
}
log_tab := DockTab{
name: "Log"
template := LogTab
kind := LogContent
}
// 4. Content templates (instantiated when tab is shown)
FileTreeContent := View{
flow: Down
width: Fill height: Fill
Label{text: "File tree here"}
}
EditorContent := View{
flow: Down
width: Fill height: Fill
Label{text: "Editor here"}
}
LogContent := View{
flow: Down
width: Fill height: Fill
Label{text: "Log here"}
}
}
Dock variants: Dock (rounded corners), DockFlat (flat style)
Properties: inherits View (flow: Overlay, align: Center). Contains bg_view (backdrop) and content (dialog body), both declared with :=.
my_modal := Modal{
content +: {
width: 300 height: Fit
RoundedView{ height: Fit
padding: 20 flow: Down spacing: 10
draw_bg.color: #333
Label{text: "Dialog Title"}
close := ButtonFlat{text: "Close"}
}
}
}
tooltip := Tooltip{}
popup := PopupNotification{
align: Align{x: 1.0 y: 0.0}
content +: { ... }
}
Properties: side (SlideSide), inherits View. Animated active float.
panel := SlidePanel{
side: SlideSide.Left // Left | Right | Top
width: 200
height: Fill
// child content
}
Properties: initial_offset, inherits View (flow: Overlay). First child = background, panel (declared with :=) = draggable overlay.
ExpandablePanel{
width: Fill height: Fill
initial_offset: 100.0
View{ height: Fit ... } // background
panel := View{ height: Fit ... } // draggable panel
}
Properties: active_page (LiveId), lazy_init. Children are page templates declared with :=.
PageFlip{
active_page := page1
page1 := View{ height: Fit ... }
page2 := View{ height: Fit ... }
}
StackNavigation{
root_view := View{ height: Fit ... }
// StackNavigationViews added as children
}
SlidesView{
slide1 := Slide{
title := H1{text: "Title"}
SlideBody{text: "Content"}
}
slide2 := SlideChapter{
title := H1{text: "Chapter"}
}
}
FileTree{}
// Driven programmatically: begin_folder/end_folder/file
draw_bg +: {
hover: instance(0.0) // per-draw-call, animatable by Animator
color: uniform(#fff) // shared across all instances of this shader variant
tex: texture_2d(float) // texture sampler
my_var: varying(vec2(0)) // vertex→pixel interpolated (set in vertex shader)
}
When to use each:
instance()— state that varies per widget (hover, down, focus, active, disabled), per-widget colors, scale/pan. Driven by the Animator system.uniform()— theme constants shared by all instances (border_size, border_radius, theme colors). Cannot be animated.
draw_bg +: {
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.box(0. 0. self.rect_size.x self.rect_size.y 4.0)
sdf.fill(#f00)
return sdf.result // already premultiplied by sdf.fill(), no Pal.premul() needed
}
}
⛔ CRITICAL: Premultiply colors returned from pixel()! When you hand-code a pixel: fn() that returns a color (not via sdf.result), you MUST premultiply the alpha. Without this, colors with alpha (e.g. #ffffff08) will render as bright white instead of a subtle tint. Always wrap your return value in Pal.premul():
pixel: fn(){
return Pal.premul(self.color.mix(self.color_hover, self.hover))
}
Note: sdf.fill() / sdf.stroke() already premultiply internally, so return sdf.result is safe without extra Pal.premul().
Common pattern — fill + border stroke:
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.box(1. 1. self.rect_size.x - 2. self.rect_size.y - 2. 4.0)
sdf.fill_keep(self.color) // fill the shape, keep it for stroke
sdf.stroke(self.border_color, 1.0) // stroke the same shape's outline
return sdf.result
}
sdf.circle(cx cy radius)
sdf.rect(x y w h)
sdf.box(x y w h border_radius)
sdf.box_all(x y w h r_lt r_rt r_rb r_lb) // per-corner radius
sdf.box_x(x y w h r_left r_right)
sdf.box_y(x y w h r_top r_bottom)
sdf.hexagon(cx cy radius)
sdf.hline(y half_height)
sdf.arc_round_caps(cx cy radius start_angle end_angle thickness)
sdf.arc_flat_caps(cx cy radius start_angle end_angle thickness)
sdf.move_to(x y)
sdf.line_to(x y)
sdf.close_path()
These operate on the current shape and the previous shape. Draw two primitives, then combine:
sdf.union() // merge shapes together (min of distances)
sdf.intersect() // keep only overlap (max of distances)
sdf.subtract() // cut current shape from previous shape
sdf.gloop(k) // smooth/gooey union with rounding factor k
sdf.blend(k) // linear blend: 0.0 = previous shape, 1.0 = current shape
Example — ring (circle with hole):
sdf.circle(cx cy outer_radius)
sdf.circle(cx cy inner_radius)
sdf.subtract()
sdf.fill(#fff)
Example — blend for toggle animation:
sdf.circle(x y r) // ring shape (from subtract above)
sdf.circle(x y r) // solid circle
sdf.blend(self.active) // animate between ring (0) and solid (1)
sdf.fill(color) // fill and reset shape
sdf.fill_keep(color) // fill, keep shape for subsequent stroke
sdf.stroke(color width) // stroke and reset shape
sdf.stroke_keep(color w) // stroke, keep shape
sdf.glow(color width) // additive glow around shape, reset
sdf.glow_keep(color w) // additive glow, keep shape
sdf.clear(color) // clear result buffer with color
sdf.translate(x y)
sdf.rotate(angle cx cy)
sdf.scale(factor cx cy)
self.pos // vec2: normalized position [0,1] (computed from clipping in vertex shader)
self.rect_size // vec2: pixel size of the drawn rect
self.rect_pos // vec2: pixel position of the drawn rect
self.dpi_factor // float: display DPI factor for high-DPI screens
self.draw_pass.time // float: elapsed time in seconds (for continuous animation)
self.draw_pass.dpi_dilate // float: DPI dilation factor for pixel-perfect strokes
self.draw_depth // float: base depth for z-ordering
self.draw_zbias // float: z-bias offset added to depth
self.geom_pos // vec2: raw geometry position [0,1] (before clipping)
Most widgets use the default vertex shader from DrawQuad. You can override it for custom geometry expansion (e.g., shadows) or DPI-aware texture coordinates:
draw_bg +: {
// custom varying to pass data from vertex to pixel shader
my_scale: varying(vec2(0))
vertex: fn() {
let dpi = self.dpi_factor
let ceil_size = ceil(self.rect_size * dpi) / dpi
self.my_scale = self.rect_size / ceil_size
return self.clip_and_transform_vertex(self.rect_pos self.rect_size)
}
pixel: fn() {
// my_scale is available here, interpolated from vertex
return Pal.premul(self.color)
}
}
self.clip_and_transform_vertex(rect_pos rect_size) is the standard helper that handles clipping, view shift (scrolling), and camera projection. Always call it in custom vertex shaders.
You can define named functions on a draw shader for reuse:
draw_bg +: {
get_color: fn() {
return self.color
.mix(self.color_hover, self.hover)
.mix(self.color_down, self.down)
}
pixel: fn() {
return Pal.premul(self.get_color())
}
}
Functions with parameters:
draw_bg +: {
get_color_at: fn(scale: vec2, pan: vec2) {
return self.my_texture.sample(self.pos * scale + pan)
}
}
Use let mut to declare mutable variables in shader code:
pixel: fn() {
let mut color = self.color
if self.hover > 0.5 {
color = self.color_hover
}
return Pal.premul(color)
}
Shaders support if/else and match on enum instance variables:
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
if self.is_vertical > 0.5 {
sdf.box(1. self.rect_size.y * self.scroll_pos 8. self.rect_size.y * self.handle_size 2.)
} else {
sdf.box(self.rect_size.x * self.scroll_pos 1. self.rect_size.x * self.handle_size 8. 2.)
}
sdf.fill(self.color)
return sdf.result
}
Declare texture samplers and sample them in pixel shaders:
draw_bg +: {
my_tex: texture_2d(float)
pixel: fn() {
let color = self.my_tex.sample(self.pos) // standard 2D sampling
return Pal.premul(color)
}
}
Alternative sampling functions:
sample2d(self.my_tex, uv) // free-function form of texture sampling
sample2d_rt(self.image, uv) // sample from render-target texture (handles Y-flip on some platforms)
mix(color1 color2 factor) // linear interpolation (free function)
color1.mix(color2 factor) // method chaining form
#f00.mix(#0f0 0.5).mix(#00f hover) // multi-chain for state interpolation
Pal.premul(color) // premultiply alpha — REQUIRED when returning from pixel()!
Pal.hsv2rgb(vec4(h s v 1.0)) // HSV to RGB conversion
Pal.rgb2hsv(color) // RGB to HSV conversion
Pal.iq(t a b c d) // Inigo Quilez cosine color palette
Pal.iq0(t) .. Pal.iq7(t) // pre-built cosine color palettes
Pal.premul() when returning from pixel: fn() (unless returning sdf.result which is already premultiplied).
Gradient pattern — use vec4(-1.0, -1.0, -1.0, -1.0) as a sentinel for "no gradient", then check with if self.color_2.x > -0.5:
color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) // sentinel: no gradient
pixel: fn() {
let mut fill = self.color
if self.color_2.x > -0.5 {
let dither = Math.random_2d(self.pos.xy) * 0.04
let dir = self.pos.y + dither
fill = mix(self.color, self.color_2, dir)
}
return Pal.premul(fill)
}
The sdf.aa field controls anti-aliasing sharpness. Default is computed from viewport. Set higher for sharper edges:
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.aa = sdf.aa * 3.0 // sharper edges (useful for small icons)
sdf.move_to(c.x - sz, c.y - sz)
sdf.line_to(c.x + sz, c.y + sz)
sdf.stroke(#fff, 0.5 + 0.5 * self.draw_pass.dpi_dilate)
return sdf.result
}
When filling with a color that is already premultiplied (e.g., from a texture sample or render target):
sdf.fill_premul(color) // fill with premultiplied color, reset shape
sdf.fill_keep_premul(color) // fill with premultiplied color, keep shape
GaussShadow.box_shadow(lower upper point sigma) // fast rectangular shadow
GaussShadow.rounded_box_shadow(lower upper point sigma corner) // rounded rectangle shadow
Used in shadow view variants (RectShadowView, RoundedShadowView) to render drop shadows efficiently.
// Custom Makepad functions
Math.random_2d(vec2) // pseudo-random 0-1 from vec2 seed (for dithering)
Math.rotate_2d(v angle) // 2D rotation of vector by angle
// Constants
PI // 3.14159...
E // 2.71828...
TORAD // degrees→radians multiplier (0.01745...)
GOLDEN // golden ratio (1.61803...)
// Standard GLSL math (all work on float, vec2, vec3, vec4)
sin(x) cos(x) tan(x) asin(x) acos(x) atan(y x)
pow(x y) sqrt(x) exp(x) exp2(x) log(x) log2(x)
abs(x) sign(x) floor(x) ceil(x) fract(x) mod(x y)
min(x y) max(x y) clamp(x min max)
step(edge x) smoothstep(edge0 edge1 x)
// Vector operations
length(v) distance(a b) dot(a b) cross(a b) normalize(v)
// Fragment-only (for advanced anti-aliasing)
dFdx(v) dFdy(v) // partial derivatives (used in text SDF rendering)
The animator drives instance() variables on draw shaders over time, enabling hover effects, transitions, and looping animations.
NOT all widgets have an animator field. If you add animator: Animator{...} to a widget that doesn't support it, the definition is silently ignored — no error, no hover, nothing happens.
Widgets that SUPPORT animator: View, SolidView, RoundedView, ScrollXView, ScrollYView, ScrollXYView, Button, ButtonFlat, ButtonFlatter, CheckBox, Toggle, RadioButton, LinkLabel, TextInput
Widgets that DO NOT support animator: Label, H1–H4, P, TextBox, Image, Icon, Markdown, Html, Slider, DropDown, Splitter, Hr, Filler
To make a Label hoverable, wrap it in a View:
View{
width: Fill height: Fit
cursor: MouseCursor.Hand
show_bg: true
draw_bg +: {
color: uniform(#0000)
color_hover: uniform(#fff2)
hover: instance(0.0)
pixel: fn(){
return Pal.premul(self.color.mix(self.color_hover, self.hover))
}
}
animator: Animator{
hover: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 0.0}}
}
on: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 1.0}}
}
}
}
Label{text: "hoverable item" draw_text.color: #fff}
}
animator: Animator{
<group_name>: {
default: @<state_name> // initial state (@ prefix required)
<state_name>: AnimatorState{
from: { ... } // transition timing
ease: <EaseFunction> // optional ease override
redraw: true // optional: force redraw each frame
apply: { ... } // target values
}
<state_name>: AnimatorState{ ... }
}
<group_name>: { ... } // multiple groups allowed
}
Each group is an independent animation track (e.g. hover, focus, active, disabled, time). Multiple groups animate simultaneously without interfering.
Controls when/how the transition plays. Keys are state names being transitioned FROM, or all as catch-all:
from: {all: Forward {duration: 0.2}} // from any state
from: {all: Snap} // instant from any state
from: {
all: Forward {duration: 0.1} // default
down: Forward {duration: 0.01} // faster when coming from "down"
}
Target values to animate TO. The structure mirrors the widget's property tree. Keys are the widget's sub-objects (like draw_bg, draw_text), values are the shader instance variables to animate:
apply: {
draw_bg: {hover: 1.0} // animate draw_bg.hover to 1.0
draw_text: {hover: 1.0} // animate draw_text.hover to 1.0
}
Multiple properties in one block:
apply: {
draw_bg: {down: 1.0, hover: 0.5}
draw_text: {down: 1.0, hover: 0.5}
}
For non-draw properties (e.g. a float field on the widget itself):
apply: {
opened: 1.0 // animate widget's own "opened" field
active: 0.0 // animate widget's own "active" field
}
Wrapping a value in snap() makes it jump instantly instead of interpolating:
apply: {
draw_bg: {down: snap(1.0), hover: 1.0} // down jumps, hover interpolates
}
Animate through multiple values over the duration using time/value pairs (times 0.0–1.0):
apply: {
draw_bg: {anim_time: timeline(0.0 0.0 1.0 1.0)} // linear 0→1
}
animator: Animator{
disabled: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.}}
apply: {
draw_bg: {disabled: 0.0}
draw_text: {disabled: 0.0}
}
}
on: AnimatorState{
from: {all: Forward {duration: 0.2}}
apply: {
draw_bg: {disabled: 1.0}
draw_text: {disabled: 1.0}
}
}
}
hover: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.1}}
apply: {
draw_bg: {down: 0.0, hover: 0.0}
draw_text: {down: 0.0, hover: 0.0}
}
}
on: AnimatorState{
from: {
all: Forward {duration: 0.1}
down: Forward {duration: 0.01}
}
apply: {
draw_bg: {down: 0.0, hover: snap(1.0)}
draw_text: {down: 0.0, hover: snap(1.0)}
}
}
down: AnimatorState{
from: {all: Forward {duration: 0.2}}
apply: {
draw_bg: {down: snap(1.0), hover: 1.0}
draw_text: {down: snap(1.0), hover: 1.0}
}
}
}
focus: {
default: @off
off: AnimatorState{
from: {all: Snap}
apply: {
draw_bg: {focus: 0.0}
draw_text: {focus: 0.0}
}
}
on: AnimatorState{
from: {all: Snap}
apply: {
draw_bg: {focus: 1.0}
draw_text: {focus: 1.0}
}
}
}
time: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.}}
apply: {}
}
on: AnimatorState{
from: {all: Loop {duration: 1.0, end: 1000000000.0}}
apply: {
draw_bg: {anim_time: timeline(0.0 0.0 1.0 1.0)}
}
}
}
}
Forward {duration: 0.2} // play once forward
Snap // instant (no interpolation)
Reverse {duration: 0.2, end: 1.0} // play in reverse
Loop {duration: 1.0, end: 1000000000.0} // repeat forward
ReverseLoop {duration: 1.0, end: 1.0} // repeat in reverse
BounceLoop {duration: 1.0, end: 1.0} // bounce back and forth
Linear // default
InQuad OutQuad InOutQuad
InCubic OutCubic InOutCubic
InQuart OutQuart InOutQuart
InQuint OutQuint InOutQuint
InSine OutSine InOutSine
InExp OutExp InOutExp
InCirc OutCirc InOutCirc
InElastic OutElastic InOutElastic
InBack OutBack InOutBack
InBounce OutBounce InOutBounce
ExpDecay {d1: 0.82, d2: 0.97, max: 100}
Pow {begin: 0.0, end: 1.0}
Bezier {cp0: 0.0, cp1: 0.0, cp2: 1.0, cp3: 1.0}
space_1 space_2 space_3
mspace_1 mspace_2 mspace_3 (uniform)
mspace_h_1 mspace_h_2 mspace_h_3 (horizontal only)
mspace_v_1 mspace_v_2 mspace_v_3 (vertical only)
corner_radius beveling tab_height splitter_size container_corner_radius dock_border_size
color_bg_app color_fg_app color_bg_container color_bg_even color_bg_odd
color_text color_text_hl color_text_disabled
color_label_inner color_label_outer (+ _hover _down _focus _active _disabled)
color_inset (+ variants) color_outset (+ variants)
color_bevel (+ variants)
color_shadow color_highlight color_makepad (#FF5C39)
color_white color_black
color_error color_warning color_panic
color_selection_focus color_cursor
color_u_1..color_u_6 (light scale) color_d_1..color_d_5 (dark scale)
color_u_hidden color_d_hidden (transparent)
color_drag_target_preview
color_val color_handle (+ _hover _focus _drag _disabled) — slider colors
color_mark_off color_mark_active (+ variants) — check/radio marks
color_app_caption_bar
font_size_1..font_size_4 font_size_p font_size_code font_size_base
font_regular font_bold font_italic font_bold_italic font_code font_icons
font_wdgt_line_spacing font_longform_line_spacing
Default Hand Arrow Text Move Wait Help NotAllowed Crosshair Grab Grabbing NResize EResize SResize WResize NsResize EwResize ColResize RowResize Hidden
Usage: cursor: MouseCursor.Hand
Stretch Horizontal Vertical Smallest Biggest Size
Horizontal Vertical
FromA(250.0) FromB(200.0) Weighted(0.5)
Left Right Top
Horizontal Vertical
Stop Once Loop Bounce Frame(0.0) Factor(0.0) OnceFps(60.0) LoopFps(60.0) BounceFps(60.0)
REMINDER: Every container below uses height: Fit — you must too!
RoundedView{
width: Fill height: Fit
padding: 15 flow: Down spacing: 8
draw_bg.color: #445
draw_bg.border_radius: 8.0
Label{text: "Card Title" draw_text.color: #fff}
}
View{
width: Fill height: Fill
flow: Right
SolidView{
width: 250 height: Fill
draw_bg.color: #222
flow: Down padding: 10
}
View{
width: Fill height: Fill
flow: Down padding: 15
}
}
Splitter{
axis: SplitterAxis.Horizontal
align: SplitterAlign.FromA(250.0)
a := sidebar
b := main
}
sidebar := View{ width: Fill height: Fill flow: Down padding: 10 }
main := View{ width: Fill height: Fill flow: Down padding: 15 }
View{ height: Fit
flow: Overlay
View{ height: Fit width: Fill ... } // base content
View{ height: Fit align: Center ... } // overlay on top
}
ScrollYView{
width: Fill height: Fill
flow: Down padding: 10 spacing: 8
Label{text: "Item 1"}
Label{text: "Item 2"}
}
Note: View{ show_bg: true } is OK here because we provide a complete custom pixel shader that overrides the ugly default.
View{
width: 200 height: 200
show_bg: true
draw_bg +: {
pixel: fn(){
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.circle(
self.rect_size.x * 0.5
self.rect_size.y * 0.5
min(self.rect_size.x self.rect_size.y) * 0.4
)
sdf.fill(#f80)
return sdf.result // already premultiplied by sdf.fill(), no Pal.premul() needed
}
}
}
Label does NOT support animator. Wrap it in a View to get hover effects. Use label := to declare the inner Label so each instance can override its text via label.text::
let HoverItem = View{
width: Fill height: Fit
padding: 8
cursor: MouseCursor.Hand
new_batch: true
show_bg: true
draw_bg +: {
color: uniform(#0000)
color_hover: uniform(#fff2)
hover: instance(0.0)
pixel: fn(){
return self.color.mix(self.color_hover, self.hover)
}
}
animator: Animator{
hover: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 0.0}}
}
on: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 1.0}}
}
}
}
label := Label{text: "item" draw_text.color: #fff}
}
RoundedView{
width: 300 height: Fit
padding: 10 flow: Down spacing: 4
new_batch: true
draw_bg.color: #222
draw_bg.border_radius: 5.0
Label{text: "Todo Items" draw_text.color: #fff}
HoverItem{label.text: "Walk the dog"}
HoverItem{label.text: "Do laundry"}
HoverItem{label.text: "Buy groceries"}
}
RectShadowView{
width: Fill height: 38.
flow: Down padding: theme.mspace_2
draw_bg +: {
shadow_color: theme.color_shadow
shadow_radius: 7.5
color: theme.color_fg_app
}
content := View{
height: Fit width: Fill
flow: Right spacing: theme.space_2
align: Align{x: 0. y: 0.5}
ButtonFlatter{text: "File"}
ButtonFlatter{text: "Edit"}
Filler{}
ButtonFlat{text: "Run"}
}
}
Make async HTTP requests from script. Responses arrive via callbacks.
let req = net.HttpRequest{
url: "https://html.duckduckgo.com/html/?q=rust+programming"
method: net.HttpMethod.GET
headers: {"User-Agent": "MakepadApp/1.0"}
}
net.http_request(req) do net.HttpEvents{
on_response: |res| {
let text = res.body.to_string() // body as string
let json = res.body.parse_json() // or parse as JSON
// res.status_code // HTTP status (200, 404, etc.)
}
on_error: |e| {
// e.message // error description
}
}
let req = net.HttpRequest{
url: "https://api.example.com/data"
method: net.HttpMethod.POST
headers: {"Content-Type": "application/json"}
body: {key: "value" count: 42}.to_json()
}
net.http_request(req) do net.HttpEvents{
on_response: |res| { /* ... */ }
on_error: |e| { /* ... */ }
}
let req = net.HttpRequest{
url: "https://api.example.com/stream"
method: net.HttpMethod.POST
is_streaming: true
body: {stream: true}.to_json()
}
var total = ""
net.http_request(req) do net.HttpEvents{
on_stream: |res| {
total += res.body.to_string() // called per chunk
}
on_complete: |res| {
// stream finished, total has all data
}
on_error: |e| { /* ... */ }
}
net.HttpMethod.GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS
DuckDuckGo provides HTML endpoints that return static HTML — no cookies, no JS, no API key:
https://html.duckduckgo.com/html/?q=QUERY— div-based, CSS classes for resultshttps://lite.duckduckgo.com/lite/?q=QUERY— table-based, ~10kB compressed
Both require a User-Agent header. Results can be parsed with parse_html().
Parse an HTML string and query it with CSS-like selectors. Call .parse_html() on any string.
let html = "<div class='box' id='main'><p>Hello</p><p class='bold'>World</p></div>"
let doc = html.parse_html()
doc.query("p") // all <p> elements (returns html handle)
doc.query("p[0]") // first <p> element
doc.query("#main") // element with id "main"
doc.query("p.bold") // <p> with class "bold"
doc.query("div > p") // direct children
doc.query("div p") // descendants
doc.query("div > *") // all direct children (wildcard)
doc.query("div").query("p") // chained queries
doc.query("p[0]").text // text content: "Hello"
doc.query("div@class") // attribute value: "box"
doc.query("div@id") // attribute value: "main"
doc.query("p.text") // array of text from all <p>: ["Hello", "World"]
doc.query("p@class") // array of class attrs from all <p>
handle.length // number of matched elements
handle.text // text content (concatenated)
handle.html // reconstructed HTML string
handle.attr("name") // attribute value (string or nil)
handle.array() // convert to array of element handles
let items = doc.query("a.result__a").array()
for item, i in items {
let title = item.text
let href = item.attr("href")
// ... use title and href
}
fn do_search(query) {
let req = net.HttpRequest{
url: "https://html.duckduckgo.com/html/?q=" + query
method: net.HttpMethod.GET
headers: {"User-Agent": "MakepadApp/1.0"}
}
net.http_request(req) do net.HttpEvents{
on_response: |res| {
let doc = res.body.to_string().parse_html()
let links = doc.query("a.result__a").array()
let snippets = doc.query("a.result__snippet").array()
for link, i in links {
let title = link.text
let url = link.attr("href")
let snippet = if i < snippets.len() snippets[i].text else ""
// ... build result list
}
}
on_error: |e| { /* handle error */ }
}
}
- ⛔ Default text color is WHITE. For light/white themes, set
draw_text.colorto a dark color (e.g.#222,#333) on ALL text elements. Otherwise text is invisible (white-on-white). - ⛔ Set
new_batch: trueon ANY View withshow_bg: truethat contains text. Makepad batches same-shader widgets into one draw call. Withoutnew_batch: true, text renders behind backgrounds (invisible text). This is especially critical for hoverable items — text vanishes on hover when the background becomes opaque. Set it on BOTH the item template AND the parent container. ⚠️ ALWAYS setheight: Fiton containers! The default isheight: Fillwhich causes 0-height (invisible UI) in this context.- ⛔ Named children in
lettemplates MUST use:=:label := Label{...},tag := Label{...},check := CheckBox{...}. Override withItem{label.text: "x"}. Without:=, text is invisible. - ⛔ Named children inside anonymous Views are UNREACHABLE. If
label :=is inside an unnamedView{},Item{label.text: "x"}fails silently. Give the View a name:texts := View{ label := Label{...} }then override withItem{texts.label.text: "x"}. - 🚫 DO NOT invent properties or syntax. Only use what's documented in this manual. No guessing.
- No commas between sibling properties (space or newline separated)
- Use commas when values contain negative numbers or could be parsed as expressions:
vec4(-1.0, -1.0, -1.0, -1.0)NOTvec4(-1.0 -1.0 -1.0 -1.0)(the parser would see-1.0 -1.0as subtraction). Safe rule: always use commas insidevec2(),vec4(), and array literals when any value is negative or an expression +:merges with parent; without it, replaces entirely:=declares named/dynamic/template children (e.g.label := Label{...})- Bare numbers for Size become
Fixed(n):width: 200=width: Size.Fixed(200) - Resources:
crate_resource("self://relative/path") - Function args in shaders: space-separated, no commas:
sdf.box(0. 0. 100. 100. 5.0) ifin shaders:if condition { ... } else { ... }(no parens around condition)forin shaders:for i in 0..4 { ... }matchin shaders:match self.block_type { Type.A => { ... } Type.B => { ... } }- Inherit + override:
theme.mspace_1{left: theme.space_2}— takes mspace_1 but overrides left - Strings use double quotes only:
text: "Hello". No single quotes, no backticks.
- Use runsplash blocks for anything visual: UI mockups, styled cards, layouts, color palettes, shader demos, button groups, form layouts, etc.
- You can have multiple runsplash blocks in a single response, mixed with normal markdown text.
- Keep splash blocks focused — one concept per block when possible.
- Use
letbindings at the top of a block to define reusable styled components, then instantiate them below. - Use theme variables (theme.color_bg_app, theme.space_2, etc.) for consistent styling.
- For simple text answers, just use normal markdown without runsplash blocks.
The Vector{} widget renders SVG-like vector graphics declaratively in Splash. It supports paths, shapes, gradients, filters, groups, transforms, and animations — all without loading external SVG files.
Vector{width: 200 height: 200 viewbox: vec4(0 0 200 200)
Rect{x: 10 y: 10 w: 80 h: 60 rx: 5 ry: 5 fill: #f80}
Circle{cx: 150 cy: 50 r: 30 fill: #08f}
Line{x1: 10 y1: 150 x2: 190 y2: 150 stroke: #fff stroke_width: 2}
}
The viewbox property defines the coordinate space as vec4(x y width height). The widget sizes itself to fit the viewbox when width: Fit and height: Fit (the defaults), or you can set explicit pixel dimensions.
All shapes support these common style properties:
| Property | Type | Default | Notes |
|---|---|---|---|
fill |
color, Gradient, RadGradient, Tween, or false |
inherited | false = no fill |
fill_opacity |
f32 | 1.0 | multiplied with fill alpha |
stroke |
color, Gradient, or Tween | none | outline color |
stroke_width |
f32 or Tween | 0.0 | outline thickness |
stroke_opacity |
f32 or Tween | 1.0 | outline alpha |
opacity |
f32 or Tween | 1.0 | overall shape opacity |
stroke_linecap |
string | "butt" | "butt", "round", "square" |
stroke_linejoin |
string | "miter" | "miter", "round", "bevel" |
transform |
Transform or array | identity | see Transforms section |
filter |
Filter ref | none | see Filters section |
shader_id |
f32 | 0.0 | for custom GPU effects on Svg widget |
Path{d: "M 10 10 L 100 100 C 50 50 200 200 300 300 Z" fill: #f00 stroke: #000 stroke_width: 2}
The d property accepts standard SVG path data strings (M, L, C, Q, A, Z, etc.).
Rect{x: 10 y: 20 w: 100 h: 50 rx: 5 ry: 5 fill: #f80 stroke: #fff stroke_width: 1}
Circle{cx: 50 cy: 50 r: 40 fill: #08f}
Ellipse{cx: 100 cy: 50 rx: 80 ry: 40 fill: #0f8}
Line{x1: 10 y1: 10 x2: 190 y2: 190 stroke: #fff stroke_width: 2 stroke_linecap: "round"}
Polyline{pts: [10 10 50 80 100 20 150 90] fill: false stroke: #ff0 stroke_width: 2}
Polygon{pts: [100 10 40 198 190 78 10 78 160 198] fill: #f0f stroke: #fff stroke_width: 1}
Group{} composes shapes and applies shared styles/transforms to all children:
Vector{width: 200 height: 200 viewbox: vec4(0 0 200 200)
Group{opacity: 0.7 transform: Rotate{deg: 15}
Rect{x: 20 y: 20 w: 60 h: 60 fill: #f00}
Circle{cx: 130 cy: 50 r: 30 fill: #0f0}
}
}
Groups can be nested. Style properties on a Group (fill, stroke, etc.) apply to its children.
Define gradients as let bindings and reference them in fill or stroke:
let my_grad = Gradient{x1: 0 y1: 0 x2: 1 y2: 1
Stop{offset: 0 color: #ff0000}
Stop{offset: 0.5 color: #00ff00}
Stop{offset: 1 color: #0000ff}
}
Vector{width: 200 height: 100 viewbox: vec4(0 0 200 100)
Rect{x: 0 y: 0 w: 200 h: 100 fill: my_grad}
}
Gradient coordinates (x1, y1, x2, y2) are in the range 0–1 (object bounding box). Stop children define color stops with offset (0–1), color, and optional opacity.
let radial = RadGradient{cx: 0.5 cy: 0.5 r: 0.5
Stop{offset: 0 color: #fff}
Stop{offset: 1 color: #000}
}
Vector{width: 200 height: 200 viewbox: vec4(0 0 200 200)
Circle{cx: 100 cy: 100 r: 90 fill: radial}
}
RadGradient properties: cx, cy (center, default 0.5), r (radius, default 0.5), fx, fy (focal point, defaults to center).
let glass = Gradient{x1: 0 y1: 0 x2: 1 y2: 1
Stop{offset: 0 color: #xffffff opacity: 0.35}
Stop{offset: 0.4 color: #xffffff opacity: 0.08}
Stop{offset: 1 color: #xffffff opacity: 0.2}
}
Define a Filter with DropShadow effects:
let shadow = Filter{
DropShadow{dx: 2 dy: 4 blur: 6 color: #000000 opacity: 0.5}
}
Vector{width: 200 height: 200 viewbox: vec4(0 0 200 200)
Rect{x: 40 y: 40 w: 120 h: 120 rx: 10 ry: 10 fill: #445 filter: shadow}
}
DropShadow properties: dx (x offset), dy (y offset), blur (blur radius), color, opacity.
Transforms can be applied to any shape or group via the transform property. Use a single transform or an array of transforms (composed left-to-right):
// Single transform
Rect{x: 0 y: 0 w: 50 h: 50 fill: #f00 transform: Rotate{deg: 45}}
// Multiple transforms (composed left-to-right)
Group{transform: [Translate{x: 100 y: 50} Scale{x: 2 y: 2} Rotate{deg: 30}]
Circle{cx: 0 cy: 0 r: 20 fill: #0ff}
}
Available transforms:
Rotate{deg: 45}— rotation in degrees. Optionalcx,cyfor rotation centerScale{x: 2 y: 1.5}— scale factors. If onlyxis given,ydefaults to the same valueTranslate{x: 100 y: 50}— translation offsetSkewX{deg: 30}— horizontal skewSkewY{deg: 15}— vertical skew
Add dur, from, to (or values), and optionally loop_ and begin to animate:
// Continuously rotating shape
Circle{cx: 100 cy: 100 r: 30 fill: #0ff
transform: Rotate{deg: 0 dur: 2.0 from: 0 to: 360 loop_: true}
}
// Animated scale
Rect{x: 50 y: 50 w: 40 h: 40 fill: #f80
transform: Scale{x: 1 dur: 1.5 from: 1 to: 2 loop_: true}
}
Use Tween{} to animate individual shape properties (fill, stroke, d, x, y, r, etc.):
// Animated path morphing
Path{d: Tween{
dur: 2.0 loop_: true
values: ["M 10 80 Q 50 10 100 80" "M 10 80 Q 50 150 100 80"]
} fill: #f0f}
// Animated fill color
Circle{cx: 50 cy: 50 r: 30
fill: Tween{dur: 1.5 loop_: true from: #ff0000 to: #0000ff}
}
// Animated stroke width
Rect{x: 10 y: 10 w: 80 h: 80
fill: false stroke: #fff
stroke_width: Tween{dur: 2.0 loop_: true from: 1 to: 5}
}
Tween properties:
from,to— start and end valuesvalues— array of keyframe values (alternative to from/to)dur— duration in secondsbegin— start delay in secondsloop_—truefor indefinite, or a number for repeat countcalc— "linear" (default), "discrete", "paced", "spline"fill_mode— "remove" (default) or "freeze"
This example from the splash demo recreates the Makepad app icon using Vector:
// Define gradients
let glass_bg = Gradient{x1: 0 y1: 0 x2: 1 y2: 1
Stop{offset: 0 color: #x556677 opacity: 0.45}
Stop{offset: 1 color: #x334455 opacity: 0.35}
}
let brain_grad = Gradient{x1: 0.5 y1: 0 x2: 0.5 y2: 1
Stop{offset: 0 color: #x77ccff}
Stop{offset: 0.4 color: #x7799ee}
Stop{offset: 0.75 color: #x8866dd}
Stop{offset: 1 color: #x9944cc}
}
let brain_glow = RadGradient{cx: 0.5 cy: 0.45 r: 0.45
Stop{offset: 0 color: #x4466ee opacity: 0.4}
Stop{offset: 1 color: #x4466dd opacity: 0.0}
}
// Define filter
let icon_shadow = Filter{
DropShadow{dx: 0 dy: 4 blur: 6 color: #x000000 opacity: 0.5}
}
Vector{width: 256 height: 256 viewbox: vec4(0 0 256 256)
// Glass background with shadow
Rect{x: 16 y: 16 w: 224 h: 224 rx: 44 ry: 44
fill: glass_bg filter: icon_shadow}
// Brain glow
Circle{cx: 128 cy: 95 r: 80 fill: brain_glow}
// Brain paths (scaled and translated group)
Group{transform: [Translate{x: 36.8 y: 11.4} Scale{x: 7.6 y: 7.6}]
Path{d: "M15.5 13a3.5 3.5 0 0 0 -3.5 3.5v1a3.5 3.5 0 0 0 7 0v-1.8"
fill: false stroke: brain_grad stroke_width: 0.35
stroke_linecap: "round" stroke_linejoin: "round"}
Path{d: "M8.5 13a3.5 3.5 0 0 1 3.5 3.5v1a3.5 3.5 0 0 1 -7 0v-1.8"
fill: false stroke: brain_grad stroke_width: 0.35
stroke_linecap: "round" stroke_linejoin: "round"}
}
// Keyboard keys
Rect{x: 73 y: 190 w: 9 h: 6 rx: 1 ry: 1 fill: #xffffff fill_opacity: 0.18}
Rect{x: 85 y: 190 w: 9 h: 6 rx: 1 ry: 1 fill: #xffffff fill_opacity: 0.18}
}
Simple SVG icons can be embedded directly as Path shapes:
// File icon (from icon_file.svg)
Vector{width: 32 height: 32 viewbox: vec4(0 0 49 49)
Path{d: "M12.069,11.678c0,-2.23 1.813,-4.043 4.043,-4.043l10.107,0l0,8.086c0,1.118 0.903,2.021 2.021,2.021l8.086,0l0,18.193c0,2.23 -1.813,4.043 -4.043,4.043l-16.171,0c-2.23,0 -4.043,-1.813 -4.043,-4.043l0,-24.257Zm24.257,4.043l-8.086,0l0,-8.086l8.086,8.086Z"}
}
// Folder icon
Vector{width: 32 height: 32 viewbox: vec4(0 0 49 49)
Path{d: "M11.884,37.957l24.257,0c2.23,0 4.043,-1.813 4.043,-4.043l0,-16.172c0,-2.23 -1.813,-4.042 -4.043,-4.042l-10.107,0c-0.638,0 -1.238,-0.297 -1.617,-0.809l-1.213,-1.617c-0.765,-1.017 -1.965,-1.617 -3.235,-1.617l-8.085,0c-2.23,0 -4.043,1.813 -4.043,4.043l0,20.214c0,2.23 1.813,4.043 4.043,4.043Z"}
}
Vector{} |
Svg{} |
|
|---|---|---|
| Input | Declarative shapes in Splash script | External .svg file via resource handle |
| Use case | Programmatic/inline vector graphics | Loading pre-made SVG assets |
| Gradients | let bindings, referenced by name |
Parsed from SVG <defs> |
| Animation | Tween{} on properties, animated transforms |
Parsed from SVG <animate> elements |
| Custom shaders | shader_id + custom get_color on Svg |
Same mechanism via draw_svg +: |
| Syntax | Vector{viewbox: ... Path{} Rect{}} |
Svg{draw_svg +: {svg: crate_resource("self://file.svg")}} |
Use Vector{} when you want to define graphics inline in your UI script. Use Svg{} when loading existing SVG files as assets.
When using hex colors containing the letter e inside script_mod!, use the #x prefix to avoid parse errors:
// These need #x prefix (contain 'e' adjacent to digits)
fill: #x2ecc71
fill: #x1e1e2e
fill: #x4466ee
// These are fine without #x (no 'e' issue)
fill: #ff4444
fill: #00ff00
The MathView{} widget renders LaTeX mathematical equations using vector glyph rendering with the NewCMMath font.
MathView{text: "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}" font_size: 14.0}
| Property | Type | Default | Notes |
|---|---|---|---|
text |
string | "" | LaTeX math expression |
font_size |
f64 | 11.0 | Font size in points |
color |
vec4 | #fff | Color of rendered math |
width |
Size | Fit | Widget width |
height |
Size | Fit | Widget height |
// Inline in a layout
View{flow: Down height: Fit spacing: 12 padding: 15
Label{text: "Quadratic Formula" draw_text.color: #aaa draw_text.text_style.font_size: 10}
MathView{text: "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}" font_size: 14.0}
Label{text: "Euler's Identity" draw_text.color: #aaa draw_text.text_style.font_size: 10}
MathView{text: "e^{i\\pi} + 1 = 0" font_size: 16.0}
Label{text: "Integral" draw_text.color: #aaa draw_text.text_style.font_size: 10}
MathView{text: "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}" font_size: 14.0}
Label{text: "Matrix" draw_text.color: #aaa draw_text.text_style.font_size: 10}
MathView{text: "\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}" font_size: 14.0}
Label{text: "Sum" draw_text.color: #aaa draw_text.text_style.font_size: 10}
MathView{text: "\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}" font_size: 14.0}
Label{text: "Maxwell's Equations" draw_text.color: #aaa draw_text.text_style.font_size: 10}
MathView{text: "\\nabla \\times \\mathbf{E} = -\\frac{\\partial \\mathbf{B}}{\\partial t}" font_size: 14.0}
}
View{width: Fill height: Fit flow: Right spacing: 15 align: Align{y: 0.5}}
MathView{text: "\\alpha + \\beta" font_size: 8.0}
MathView{text: "\\alpha + \\beta" font_size: 12.0}
MathView{text: "\\alpha + \\beta" font_size: 18.0}
MathView{text: "\\alpha + \\beta" font_size: 24.0}
Fractions & Roots:
\frac{a}{b}, \dfrac{a}{b}, \tfrac{a}{b}, \sqrt{x}, \sqrt[n]{x}
Subscripts & Superscripts:
x_i, x^2, x_i^2, a_{n+1}
Greek Letters (lowercase):
\alpha, \beta, \gamma, \delta, \epsilon, \zeta, \eta, \theta, \lambda, \mu, \nu, \xi, \pi, \rho, \sigma, \tau, \phi, \chi, \psi, \omega
Greek Letters (uppercase):
\Gamma, \Delta, \Theta, \Lambda, \Xi, \Pi, \Sigma, \Phi, \Psi, \Omega
Big Operators:
\sum, \prod, \int, \iint, \iiint, \oint, \bigcup, \bigcap, \bigoplus, \bigotimes
Relations:
=, \neq, <, >, \leq, \geq, \sim, \approx, \equiv, \subset, \supset, \in, \notin
Arrows:
\leftarrow, \rightarrow, \leftrightarrow, \Leftarrow, \Rightarrow, \Leftrightarrow, \mapsto
Accents:
\hat{x}, \bar{x}, \tilde{x}, \vec{x}, \dot{x}, \ddot{x}, \overline{x}, \underline{x}
Delimiters (auto-sizing with \left...\right):
\left( \right), \left[ \right], \left\{ \right\}, \left| \right|, \langle \rangle, \lfloor \rfloor, \lceil \rceil
Matrices:
\begin{pmatrix} a & b \\ c & d \end{pmatrix} % parentheses
\begin{bmatrix} a & b \\ c & d \end{bmatrix} % brackets
\begin{vmatrix} a & b \\ c & d \end{vmatrix} % determinant bars
\begin{cases} a & \text{if } x > 0 \\ b & \text{otherwise} \end{cases}
Styles:
\mathbf{x} (bold), \mathit{x} (italic), \mathrm{x} (roman), \mathcal{x} (calligraphic), \mathbb{R} (blackboard bold), \mathfrak{g} (fraktur)
Spacing:
\, (thin), \: (medium), \; (thick), \! (negative thin), \quad, \qquad
Text & Operators:
\text{...}, \sin, \cos, \tan, \log, \ln, \exp, \lim, \min, \max, \det
Dots:
\ldots, \cdots, \vdots, \ddots
Misc Symbols:
\infty, \partial, \nabla, \forall, \exists, \emptyset, \pm, \mp, \times, \div, \cdot
- MathView is read-only — no selection or editing
- Uses Display math style (large operators centered)
- Backslashes must be escaped as
\\in Splash strings - The widget sizes itself to fit the rendered equation by default (
width: Fit,height: Fit) - An empty
textproduces no output
The MapView{} widget renders interactive geographic maps with OpenStreetMap data. It supports panning, zooming, street labels, and light/dark themes.
MapView renders map tiles using custom GPU geometry (DrawMapVector) and a full-rect background (DrawColor). Because of Makepad's draw batching, the map's draw calls can merge with sibling widgets' draw calls, causing the map to render on top of other UI elements (headers, labels, buttons) even though they appear earlier in the widget tree.
You MUST wrap MapView in a container with new_batch: true whenever MapView has sibling widgets (headers, toolbars, labels, etc.) in the same parent:
// CORRECT — new_batch on parent prevents map from drawing over header
RoundedView{
width: Fill height: Fit
flow: Down spacing: 0
new_batch: true
draw_bg.color: #1a1a2e
draw_bg.border_radius: 12.0
RoundedView{
width: Fill height: Fit
padding: 12
new_batch: true
draw_bg.color: #16213e
draw_bg.border_radius: 12.0
Label{text: "Map Explorer" draw_text.color: #xeee draw_text.text_style.font_size: 13}
}
MapView{
width: Fill height: 500
dark_theme: true
}
}
// WRONG — map draws over the header label because parent lacks new_batch
RoundedView{
flow: Down
RoundedView{
Label{text: "Header" draw_text.color: #fff}
}
MapView{width: Fill height: 500}
}
MapView has NO intrinsic content size. It cannot size itself. You MUST give it a fixed pixel height:
// CORRECT — always use a fixed pixel height
MapView{width: Fill height: 500}
// WRONG — height: Fill in a Fit parent = 0px = invisible map
MapView{width: Fill height: Fill}
// WRONG — MapView has no intrinsic height, Fit = 0px = invisible map
MapView{width: Fill height: Fit}
Why not height: Fill? Your output renders inside a height: Fit container. Fill inside Fit = circular dependency = 0 height = invisible. This is the #1 MapView mistake.
Why not height: Fit? MapView renders tiles on a canvas — it has no content to measure, so Fit = 0 height = invisible.
Always use a fixed height like height: 400, height: 500, or height: 600.
MapView{
width: Fill
height: 500
}
This creates a map centered on Amsterdam at zoom level 14 with the default light theme.
| Property | Type | Default | Notes |
|---|---|---|---|
center_lon |
f64 | 4.9041 | Longitude of map center |
center_lat |
f64 | 52.3676 | Latitude of map center |
zoom |
f64 | 14.0 | Initial zoom level |
min_zoom |
f64 | 11.0 | Minimum zoom allowed |
max_zoom |
f64 | 17.0 | Maximum zoom allowed |
dark_theme |
bool | false | Light or dark theme |
use_network |
bool | false | Enable online Overpass API |
use_local_mbtiles |
bool | true | Enable local .mbtiles tiles |
width |
Size | Fill | Widget width |
height |
Size | — | Must be a fixed pixel value (e.g. 500). Never Fit or Fill. |
Source mode rules: You can only use ONE source at a time. If both use_network and use_local_mbtiles are true, the widget silently forces offline-only mode. If both are false, it silently forces offline mode. To use network, set use_network: true AND use_local_mbtiles: false.
- Click + Drag — Pan the map
- Scroll wheel — Zoom in/out (centered on cursor)
- T key — Toggle light/dark theme
// Default map (Amsterdam, light theme, offline tiles)
MapView{width: Fill height: 500}
// Custom center and zoom (e.g. New York — requires network)
MapView{
width: Fill height: 500
center_lon: -73.9857
center_lat: 40.7484
zoom: 15.0
use_network: true
use_local_mbtiles: false
}
// Dark theme
MapView{
width: Fill height: 500
dark_theme: true
}
// Map with header bar (note new_batch on both parent and header)
RoundedView{
width: Fill height: Fit
flow: Down spacing: 0
new_batch: true
draw_bg.color: #1a1a2e
draw_bg.border_radius: 12.0
RoundedView{
width: Fill height: Fit
padding: Inset{top: 12 bottom: 12 left: 16 right: 16}
flow: Right
align: Align{y: 0.5}
new_batch: true
draw_bg.color: #16213e
draw_bg.border_radius: 12.0
Label{text: "Map Explorer" draw_text.color: #xeee draw_text.text_style.font_size: 13}
Filler{}
Label{text: "scroll to zoom | drag to pan | T = theme" draw_text.color: #556 draw_text.text_style.font_size: 9}
}
MapView{
width: Fill height: 500
dark_theme: true
}
}
// Sidebar + map using Splitter
Splitter{
axis: SplitterAxis.Horizontal
align: SplitterAlign.FromA(300.0)
a := SolidView{
width: Fill height: Fill
draw_bg.color: #222
flow: Down padding: 15 spacing: 10
new_batch: true
Label{text: "Map Controls" draw_text.color: #fff draw_text.text_style.font_size: 14}
Label{text: "Drag to pan, scroll to zoom" draw_text.color: #888 draw_text.text_style.font_size: 10}
Label{text: "Press T to toggle theme" draw_text.color: #888 draw_text.text_style.font_size: 10}
}
b := MapView{width: Fill height: Fill}
}
Offline mode (default): Loads tiles from a local .mbtiles file at local/noord-holland-shortbread-1.0.mbtiles. Covers the Noord Holland region (Amsterdam area). Zoom range 0–14.
Online mode (use_network: true, use_local_mbtiles: false): Queries the Overpass API for OpenStreetMap data. Covers any location worldwide but requires internet and may be slower.
use_network: true and use_local_mbtiles: true silently disables network. For non-Amsterdam locations, you MUST set use_network: true AND use_local_mbtiles: false.
MapView includes built-in light and dark themes with colors for:
- Buildings — residential, commercial structures
- Water — rivers, lakes, seas
- Land use — residential, forest, industrial, commercial areas
- Leisure — parks, gardens, pitches
- Roads — motorways, primary, secondary, tertiary, residential, footways
- Waterways — rivers, canals, streams
- Railways — rail lines with dashed styling
- Labels — street names placed along road paths
- MapView defaults to offline mode with pre-bundled Amsterdam region tiles
- Street labels appear at zoom level 13 and above
- Always use a fixed pixel height (e.g.
height: 500) — bothheight: Fitandheight: Fillproduce 0px (invisible) in the splash rendering context - Never use
height: Fit— MapView has no intrinsic height; it collapses to 0 - Never use
height: Fill— the splash output renders inside aFitparent, soFillinsideFit= 0 height - Always set
new_batch: trueon any parent container that has MapView alongside other widgets (Labels, buttons, etc.) — otherwise the map's GPU draws will render on top of sibling UI - For online mode, set
use_network: trueANDuse_local_mbtiles: false - For locations outside Amsterdam in offline mode, nothing will render — you must enable network mode
- The
zoomvalue is clamped to[min_zoom, max_zoom]automatically