855 lines
32 KiB
Markdown
855 lines
32 KiB
Markdown
# Studio Remote Runbook
|
|
|
|
## Execution Policy
|
|
- Visual UI programs must be launched and controlled through the Makepad Studio remote protocol.
|
|
- Always use release builds for runtime validation, profiling, benchmarks, timing checks, or any performance-sensitive command. Use `--release` unless the user explicitly asks for a debug build.
|
|
- Do not use mount observation or runnable discovery from the bridge client. The bridge must not claim mount ownership from Studio desktop.
|
|
- Do not launch UI programs with raw `cargo run`, `cargo makepad`, or ad hoc cargo invocation when a runnable item exists.
|
|
- Do not use bridge `Cargo` requests to run applications. Only launch apps from runnable items via bridge `RunItem`.
|
|
- For UI runnable targets, do not prebuild or precheck the app from the shell before launching it in Studio. Let the Studio `RunItem` build be the single build path so Cargo fingerprints, env vars, target dirs, and flags stay identical.
|
|
- Before starting a new UI run for the same target, send `ClearBuild` for the previous build so Studio stops it and removes its run/log/profiler tabs.
|
|
- `cargo check` or `cargo build` never counts as UI verification. After changing UI/runtime code, you must clear the old build and start a fresh Studio run before trusting screenshots, widget dumps, or interaction results.
|
|
- Do not keep inspecting an older already-running app after code changes. Re-run the target and verify against the new `build_id`.
|
|
- Command-line-only tasks (builds, tests, linting, file ops, grep/ripgrep, etc.) can be run directly in the shell.
|
|
- Prefer studio remote control for any workflow that needs screenshots, widget queries, clicks, typing, or runtime UI inspection.
|
|
- Before using Studio protocol tools (`FindInFiles`, `ReadTextRange`, `WidgetTreeDump`, `WidgetQuery`, `Screenshot`, `Click`, `TypeText`, `Return`), always start one persistent Studio remote bridge process and reuse it for the entire interaction.
|
|
- When adding a new example crate, update both the Cargo workspace and `makepad.splash` so Studio exposes the new example as a runnable item.
|
|
|
|
## Assumptions
|
|
- Studio is started manually by the user.
|
|
- Studio remote target is `ip:port` only (no `http://`, no `ws://`), normally `127.0.0.1:8001`.
|
|
- Use `127.0.0.1:8002` only if Studio reports fallback because `8001` is occupied.
|
|
- Keep one persistent studio remote process for the whole interaction.
|
|
|
|
## Start Studio Remote
|
|
- Command:
|
|
- `target/release/cargo-makepad studio --studio=127.0.0.1:8001`
|
|
- Send newline-delimited JSON requests on stdin.
|
|
- Read newline-delimited JSON responses on stdout.
|
|
- Protocol shape is raw `ClientToHub` requests on stdin and filtered `HubToClient` responses on stdout.
|
|
- Do not send `ObserveMount` from the bridge. It can take `primary` UI ownership for the mount and divert RunView/framebuffer traffic away from Studio desktop.
|
|
|
|
## Request Protocol (JSON Lines)
|
|
- `{"ListBuilds":[]}`
|
|
- `{"ClearBuild":{"build_id":[6]}}` stops a running build and immediately clears its Studio UI tabs; use this before rerunning the same app.
|
|
- `{"StopBuild":{"build_id":[6]}}` stops/kills a running build but does not clear Studio tabs.
|
|
- `{"RunItem":{"mount":"makepad","name":"makepad-example-todo"}}`
|
|
- `{"RunItem":{"mount":"makepad","name":"makepad-example-xr-quest"}}`
|
|
- `{"FindInFiles":{"mount":"makepad","pattern":"ClientToHub::","is_regex":false,"glob":null,"max_results":200}}`
|
|
- `{"FindInFiles":{"mount":"makepad","pattern":"ClientToHub::(FindInFiles|ReadTextRange)","is_regex":true,"glob":"**/*.rs","max_results":200}}`
|
|
- `{"ReadTextRange":{"path":"makepad/studio/backend/src/dispatch.rs","start_line":640,"end_line":720}}`
|
|
- `{"WidgetTreeDump":{"build_id":[6]}}`
|
|
- `{"WidgetQuery":{"build_id":[6],"query":"id:todo_input"}}`
|
|
- `{"Screenshot":{"build_id":[6],"kind_id":0}}` (`kind_id` optional; defaults to `0`)
|
|
- `{"Click":{"build_id":[6],"x":1274,"y":342}}`
|
|
- `{"TypeText":{"build_id":[6],"text":"hello"}}`
|
|
- `{"Return":{"build_id":[6],"auto_dump":false}}`
|
|
- `{"ForwardToApp":{"build_id":[6],"msg_bin":[...]}}` (advanced; binary payload)
|
|
|
|
## `StudioToApp` API (Updated)
|
|
- The studio remote bridge supports raw app event passthrough via `UIToStudio::ForwardToApp`.
|
|
- Current `StudioToApp` variants include:
|
|
- `Screenshot`, `WidgetTreeDump`, `KeepAlive`, `LiveChange`, `Swapchain`, `WindowGeomChange`, `Tick`
|
|
- `MouseDown`, `MouseUp`, `MouseMove`, `Scroll`
|
|
- `KeyDown`, `KeyUp`, `TextInput`, `TextCopy`, `TextCut`
|
|
- `None`, `Kill`
|
|
- Use direct studio remote requests (`Screenshot`, `WidgetTreeDump`, `Click`, `TypeText`, `Return`) for normal automation.
|
|
- Use raw `StudioToApp` only for low-level event injection/debugging.
|
|
|
|
## Response Notes (Current)
|
|
- Bridge stdout is filtered to: `Hello`, `Error`, `TextFileRead`, `TextFileRange`, `FindFileResults`, `SearchFileResults`, `Builds`, `RunItems`, `BuildStarted`, `BuildStopped`, `BuildCleared`, `AppStarted`, `RunViewCreated`, `QueryLogResults`, `Screenshot`, `WidgetTreeDump`, `WidgetQuery`, `QueryCancelled`.
|
|
- `BuildCleared` is a Studio frontend cleanup signal routed to the primary UI for the build's mount; bridge clients should not wait for it before starting the next run.
|
|
- `RunViewFrame` and the terminal stream are not exposed by the bridge.
|
|
- `Screenshot` responses include file metadata (`path`, `width`, `height`) and not inline PNG bytes.
|
|
- `WidgetTreeDump` responses include text dump content keyed by `request_id`.
|
|
- `FindInFiles` responds as `SearchFileResults` with concise entries (`path`, `line`, `column`, `line_text`) and `done`.
|
|
- `FindInFiles` defaults to searching only `.rs`, `.md`, `.toml` files unless `glob` is provided.
|
|
- `ReadTextRange` responds as `TextFileRange` with `path`, requested `start_line`/`end_line`, `total_lines`, and `content`.
|
|
- Query-scoped responses are lane-filtered by `query_id.client_id`; only this bridge client's query results are emitted.
|
|
- Build ids and query ids are `QueryId` tuple structs, so JSON encodes them as one-element arrays like `[6]`.
|
|
- `FindInFiles`/`SearchFiles` execution is worker-pooled in backend (not main dispatch thread).
|
|
|
|
## Recommended Control Flow
|
|
1. Start studio remote process once.
|
|
2. Determine the target runnable item name locally from the repo or from the user request.
|
|
3. Call `ListBuilds` and find any existing build for the same runnable item.
|
|
4. Send `ClearBuild` for that old `build_id`; do not wait for an acknowledgment before the next launch.
|
|
5. Start the new UI app through `RunItem`, and wait for `BuildStarted` and `AppStarted`.
|
|
6. After any code change that affects runtime/UI behavior, repeat steps 3-5 before doing screenshots, widget dumps, clicks, or visual conclusions.
|
|
7. For code search, use `FindInFiles` first, then `ReadTextRange` to window exact regions.
|
|
8. Use direct shell cargo commands for non-launch tasks such as `check`, `build`, `test`, or `bench`.
|
|
9. Use `WidgetQuery` / `WidgetTreeDump` to get click targets.
|
|
10. For text input, click field first, then send text, then return.
|
|
11. Keep control packets compact (`auto_dump:false` on click/type/return for low latency).
|
|
|
|
## `RunItem` Launch
|
|
- `RunItem` executes a Studio-defined runnable item by name.
|
|
- Use the runnable item name shown in Studio, not a Cargo package name.
|
|
- `RunItem` does not implicitly replace an older build tab; agents should clear the old build themselves first with `ClearBuild`.
|
|
|
|
## One-Flow Input Burst
|
|
- Send this as one stdin write (multiple JSON lines, no sleeps):
|
|
- `Click` (input field center)
|
|
- `TypeText`
|
|
- `Return`
|
|
- Then request `WidgetTreeDump` or `Screenshot` to confirm.
|
|
|
|
## Coordinates
|
|
- Use coordinates from dump as-is.
|
|
- `W3` dump uses integer pixel coordinates in the same space expected by `Click`.
|
|
- Do not apply extra DPI math in the agent loop.
|
|
|
|
## Reliability Notes
|
|
- `Screenshot` can arrive before visible redraw after rapid input bursts.
|
|
- If screenshot looks stale, request a follow-up `WidgetTreeDump`/`Screenshot`.
|
|
- If input does nothing:
|
|
- Verify `build_id` with `ListBuilds`.
|
|
- Refresh dump and retry click on input before typing.
|
|
- If request errors with no active websocket:
|
|
- app is not connected yet; wait for startup completion and retry.
|
|
|
|
|
|
## CLAUDE.md Body
|
|
The following is the current body of CLAUDE.md included verbatim for agent guidance parity.
|
|
|
|
# Makepad Project Guide
|
|
|
|
## Important: When Converting Syntax
|
|
|
|
**Always search for existing usage patterns in the NEW crates (widgets, code_editor, studio) before making syntax changes.** The old `widgets` and `live_design!` syntax is deprecated. When unsure about the correct syntax for something, grep for similar usage in `widgets/src/` to find the correct pattern.
|
|
|
|
```bash
|
|
# Example: find how texture declarations work in new system
|
|
grep -r "texture_2d" widgets/src/
|
|
```
|
|
|
|
**Critical: Always use `Name: value` syntax, never `Name = value`.** The old `Key = Value` syntax no longer works. For named widget instances, use `name := Type{...}` syntax.
|
|
|
|
## Running UI Programs
|
|
|
|
Use the Studio bridge runnable-item flow instead of launching UI apps directly from the shell:
|
|
|
|
1. Start the Studio remote bridge once.
|
|
2. Determine the runnable item name locally.
|
|
3. If an older instance is still running, clear it with `{"ClearBuild":{"build_id":[N]}}` and launch the replacement immediately without waiting for an acknowledgment.
|
|
4. Launch it with `{"RunItem":{"mount":"makepad","name":"<runnable-name>"}}`.
|
|
5. After editing UI/runtime code, do not inspect the previously running build. Always verify against the newly started build id from step 4.
|
|
|
|
Do not use `ObserveMount` from the bridge. That call is for mount ownership/subscription and can steal RunView/framebuffer routing away from Studio desktop.
|
|
|
|
Use direct shell cargo commands only for non-UI tasks such as library checks, tests, and file/search operations. Do not run shell `cargo check`, `cargo build`, or `cargo run` for UI runnable targets that will be launched via Studio.
|
|
|
|
When those non-UI tasks are used for runtime behavior or performance measurements, prefer their release variants (`cargo run --release`, `cargo test --release`, `cargo build --release`).
|
|
|
|
## Cargo.toml Setup
|
|
|
|
```toml
|
|
[package]
|
|
name = "makepad-example-myapp"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
|
|
[dependencies]
|
|
makepad-widgets = { path = "../../widgets" }
|
|
```
|
|
|
|
|
|
## Widgets DSL (script_mod!)
|
|
|
|
The new DSL uses `script_mod!` macro with runtime script evaluation instead of the old `live_design!` compile-time macros.
|
|
|
|
### Imports and App Setup
|
|
|
|
```rust
|
|
use makepad_widgets::*;
|
|
|
|
app_main!(App);
|
|
|
|
script_mod!{
|
|
use mod.prelude.widgets.*
|
|
|
|
load_all_resources() do #(App::script_component(vm)){
|
|
ui: Root{
|
|
main_window := Window{
|
|
window.inner_size: vec2(800, 600)
|
|
body +: {
|
|
// UI content here
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
fn run(vm: &mut ScriptVm) -> Self {
|
|
crate::makepad_widgets::script_mod(vm); // Register all widgets
|
|
// Platform-specific initialization goes here (e.g., vm.cx().start_stdin_service() for macos)
|
|
App::from_script_mod(vm, self::script_mod)
|
|
}
|
|
}
|
|
|
|
#[derive(Script, ScriptHook)]
|
|
pub struct App {
|
|
#[live] ui: WidgetRef,
|
|
}
|
|
|
|
impl MatchEvent for App {
|
|
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
|
|
// Handle widget actions
|
|
}
|
|
}
|
|
|
|
impl AppMain for App {
|
|
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
|
|
self.match_event(cx, event);
|
|
self.ui.handle_event(cx, event, &mut Scope::empty());
|
|
}
|
|
}
|
|
```
|
|
|
|
### Available Widgets (widgets/src/lib.rs)
|
|
|
|
Core: `View`, `SolidView`, `RoundedView`, `ScrollXView`, `ScrollYView`, `ScrollXYView`
|
|
Text: `Label`, `H1`, `H2`, `H3`, `LinkLabel`, `TextInput`
|
|
Buttons: `Button`, `ButtonFlat`, `ButtonFlatter`
|
|
Toggles: `CheckBox`, `Toggle`, `RadioButton`
|
|
Input: `Slider`, `DropDown`
|
|
Layout: `Splitter`, `FoldButton`, `FoldHeader`, `Hr`
|
|
Lists: `PortalList`
|
|
Navigation: `StackNavigation`, `ExpandablePanel`
|
|
Overlays: `Modal`, `Tooltip`, `PopupNotification`
|
|
Dock: `Dock`, `DockSplitter`, `DockTabs`, `DockTab`
|
|
Media: `Image`, `Icon`, `LoadingSpinner`
|
|
Special: `FileTree`, `PageFlip`, `CachedWidget`
|
|
Window: `Window`, `Root`
|
|
Markup: `Html`, `Markdown` (feature-gated)
|
|
|
|
### Widget Definition Pattern
|
|
|
|
```rust
|
|
// Rust struct
|
|
#[derive(Script, ScriptHook, Widget)]
|
|
pub struct MyWidget {
|
|
#[source] source: ScriptObjectRef, // Required for script integration
|
|
#[walk] walk: Walk,
|
|
#[layout] layout: Layout,
|
|
#[redraw] #[live] draw_bg: DrawQuad,
|
|
#[live] draw_text: DrawText,
|
|
#[rust] my_state: i32, // Runtime-only field
|
|
}
|
|
|
|
// For widgets with animations, add Animator derive:
|
|
#[derive(Script, ScriptHook, Widget, Animator)]
|
|
pub struct AnimatedWidget {
|
|
#[source] source: ScriptObjectRef,
|
|
#[apply_default] animator: Animator,
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Script Module Structure
|
|
|
|
```rust
|
|
script_mod!{
|
|
use mod.prelude.widgets_internal.* // For internal widget definitions
|
|
use mod.widgets.* // Access other widgets
|
|
|
|
// Register base widget (connects Rust struct to script)
|
|
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
|
|
|
|
// Create styled variant with defaults
|
|
mod.widgets.MyWidget = set_type_default() do mod.widgets.MyWidgetBase{
|
|
width: Fill
|
|
height: Fit
|
|
padding: theme.space_2
|
|
|
|
draw_bg +: {
|
|
color: theme.color_bg_app
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Key Syntax Differences (Old vs New)
|
|
|
|
| Old (live_design!) | New (script_mod!) |
|
|
|-------------------|-------------------|
|
|
| `<BaseWidget>` | `mod.widgets.BaseWidget{ }` |
|
|
| `{{StructName}}` | `#(Struct::register_widget(vm))` |
|
|
| `(THEME_COLOR_X)` | `theme.color_x` |
|
|
| `<THEME_FONT>` | `theme.font_regular` |
|
|
| `instance hover: 0.0` | `hover: instance(0.0)` |
|
|
| `uniform color: #fff` | `color: uniform(#fff)` |
|
|
| `draw_bg: { }` (replace) | `draw_bg +: { }` (merge) |
|
|
| `default: off` | `default: @off` |
|
|
| `fn pixel(self)` | `pixel: fn()` |
|
|
| `item.apply_over(cx, live!{...})` | `script_apply_eval!(cx, item, {...})` |
|
|
|
|
### Runtime Property Updates with script_apply_eval!
|
|
|
|
Use `script_apply_eval!` macro to dynamically update widget properties at runtime:
|
|
```rust
|
|
// Old system (live! macro with apply_over)
|
|
item.apply_over(cx, live!{
|
|
height: (height)
|
|
draw_bg: {is_even: (if is_even {1.0} else {0.0})}
|
|
});
|
|
|
|
// New system (script_apply_eval! macro)
|
|
script_apply_eval!(cx, item, {
|
|
height: #(height)
|
|
draw_bg: {is_even: #(if is_even {1.0} else {0.0})}
|
|
});
|
|
|
|
// For colors, use #(color) syntax
|
|
let color = self.color_focus;
|
|
script_apply_eval!(cx, item, {
|
|
draw_bg: {
|
|
color: #(color)
|
|
}
|
|
});
|
|
```
|
|
|
|
Note: In `script_apply_eval!`, use `#(expr)` for Rust expression interpolation instead of `(expr)`.
|
|
|
|
### Theme Access
|
|
|
|
Always use `theme.` prefix:
|
|
```rust
|
|
color: theme.color_bg_app
|
|
padding: theme.space_2
|
|
font_size: theme.font_size_p
|
|
text_style: theme.font_regular
|
|
```
|
|
|
|
### Property Merging with `+:`
|
|
|
|
The `+:` operator merges with parent instead of replacing:
|
|
```rust
|
|
mod.widgets.MyButton = mod.widgets.Button{
|
|
draw_bg +: {
|
|
color: #f00 // Only overrides color, keeps other draw_bg properties
|
|
}
|
|
}
|
|
```
|
|
|
|
### Shader Instance vs Uniform
|
|
|
|
- `instance(value)` - Per-draw-call value (can vary per widget instance)
|
|
- `uniform(value)` - Shared across all instances using same shader
|
|
|
|
```rust
|
|
draw_bg +: {
|
|
hover: instance(0.0) // Each button has its own hover state
|
|
color: uniform(theme.color_x) // Shared base color
|
|
color_hover: instance(theme.color_y) // Per-instance if color varies
|
|
}
|
|
```
|
|
|
|
### Animator Definition
|
|
|
|
```rust
|
|
animator: Animator{
|
|
hover: {
|
|
default: @off
|
|
off: AnimatorState{
|
|
from: {all: Forward {duration: 0.1}}
|
|
apply: {
|
|
draw_bg: {hover: 0.0}
|
|
draw_text: {hover: 0.0}
|
|
}
|
|
}
|
|
on: AnimatorState{
|
|
from: {all: Snap} // Instant transition
|
|
apply: {
|
|
draw_bg: {hover: 1.0}
|
|
draw_text: {hover: 1.0}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Shader Functions
|
|
|
|
```rust
|
|
draw_bg +: {
|
|
pixel: fn() {
|
|
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
|
|
sdf.box(0.0, 0.0, self.rect_size.x, self.rect_size.y, 4.0)
|
|
sdf.fill(self.color.mix(self.color_hover, self.hover))
|
|
return sdf.result
|
|
}
|
|
}
|
|
```
|
|
|
|
Note: Use `.method()` not `::method()` in shaders.
|
|
|
|
### Color Mixing (Method Chaining)
|
|
|
|
```rust
|
|
// Old nested style (avoid)
|
|
mix(mix(mix(color1, color2, hover), color3, down), color4, focus)
|
|
|
|
// New chained style (preferred)
|
|
color1.mix(color2, hover).mix(color3, down).mix(color4, focus)
|
|
```
|
|
|
|
### App Structure Pattern
|
|
|
|
```rust
|
|
script_mod!{
|
|
use mod.prelude.widgets.*
|
|
|
|
load_all_resources() do #(App::script_component(vm)){
|
|
ui: Root{
|
|
main_window := Window{
|
|
window.inner_size: vec2(1000, 700)
|
|
body +: {
|
|
// Your UI here
|
|
MyWidget{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
fn run(vm: &mut ScriptVm) -> Self {
|
|
crate::makepad_widgets::script_mod(vm);
|
|
// Platform-specific initialization (e.g., vm.cx().start_stdin_service() for macos)
|
|
App::from_script_mod(vm, self::script_mod)
|
|
}
|
|
}
|
|
|
|
#[derive(Script, ScriptHook)]
|
|
pub struct App {
|
|
#[live] ui: WidgetRef,
|
|
}
|
|
|
|
impl MatchEvent for App {
|
|
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
|
|
if self.ui.button(ids!(my_button)).clicked(actions) {
|
|
log!("Button clicked!");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AppMain for App {
|
|
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
|
|
self.match_event(cx, event);
|
|
self.ui.handle_event(cx, event, &mut Scope::empty());
|
|
}
|
|
}
|
|
```
|
|
|
|
### Widget ID References
|
|
|
|
Use `:=` for named widget instances:
|
|
```rust
|
|
// In DSL
|
|
my_button := Button{text: "Click"}
|
|
|
|
// In Rust code
|
|
self.ui.button(ids!(my_button)).clicked(actions)
|
|
```
|
|
|
|
### Template Definitions in Dock
|
|
|
|
Templates inside Dock are local; use `let` bindings at script level for reusable components:
|
|
```rust
|
|
script_mod!{
|
|
// Reusable at script level
|
|
let MyPanel = SolidView{
|
|
width: Fill
|
|
height: Fill
|
|
// ...
|
|
}
|
|
|
|
// Use directly
|
|
body +: {
|
|
MyPanel{} // Works because it's a let binding
|
|
}
|
|
}
|
|
```
|
|
|
|
### Custom Draw Widget Example
|
|
|
|
```rust
|
|
#[derive(Script, ScriptHook, Widget)]
|
|
pub struct CustomDraw {
|
|
#[walk] walk: Walk,
|
|
#[layout] layout: Layout,
|
|
#[redraw] #[live] draw_quad: DrawQuad,
|
|
#[rust] area: Area,
|
|
}
|
|
|
|
impl Widget for CustomDraw {
|
|
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
|
|
cx.begin_turtle(walk, self.layout);
|
|
let rect = cx.turtle().rect();
|
|
self.draw_quad.draw_abs(cx, rect);
|
|
cx.end_turtle_with_area(&mut self.area);
|
|
DrawStep::done()
|
|
}
|
|
|
|
fn handle_event(&mut self, _cx: &mut Cx, _event: &Event, _scope: &mut Scope) {}
|
|
}
|
|
```
|
|
|
|
### Script Object Storage: map vs vec
|
|
|
|
In script objects, properties are stored in two different places:
|
|
- **`map`**: Contains `key: value` pairs (regular properties)
|
|
- **`vec`**: Contains named template items (via `:=` syntax)
|
|
|
|
This distinction is important when working with `on_after_apply` or inspecting script objects directly.
|
|
|
|
### Templates in List Widgets (PortalList, FlatList)
|
|
|
|
In list widgets, named IDs (using `:=`) define **templates** that are stored in the widget's `templates` HashMap. These are NOT regular properties - they go into the script object's vec and are collected via `on_after_apply`.
|
|
|
|
```rust
|
|
// In script_mod! - defining templates for a list
|
|
my_list := PortalList {
|
|
// Regular properties (go into struct fields)
|
|
width: Fill
|
|
height: Fill
|
|
scroll_bar: mod.widgets.ScrollBar {}
|
|
|
|
// Templates (named with :=) - stored in templates HashMap, NOT struct fields
|
|
Item := View {
|
|
height: 40
|
|
title := Label { text: "Default" }
|
|
}
|
|
Header := View {
|
|
draw_bg: { color: #333 }
|
|
}
|
|
}
|
|
```
|
|
|
|
The templates are collected in `on_after_apply`:
|
|
```rust
|
|
impl ScriptHook for PortalList {
|
|
fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue) {
|
|
if let Some(obj) = value.as_object() {
|
|
vm.vec_with(obj, |_vm, vec| {
|
|
for kv in vec {
|
|
if let Some(id) = kv.key.as_id() {
|
|
self.templates.insert(id, kv.value);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Then used during drawing:
|
|
```rust
|
|
while let Some(item_id) = list.next_visible_item(cx) {
|
|
let item = list.item(cx, item_id, id!(Item));
|
|
item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id));
|
|
item.draw_all(cx, &mut Scope::empty());
|
|
}
|
|
```
|
|
|
|
**Key distinction**: Regular properties like `scroll_bar: mod.widgets.ScrollBar {}` are applied directly to struct fields. Template definitions like `Item := View {...}` are stored separately for dynamic instantiation.
|
|
|
|
### PortalList Usage
|
|
|
|
```rust
|
|
#[derive(Script, ScriptHook, Widget)]
|
|
pub struct MyList {
|
|
#[deref] view: View,
|
|
}
|
|
|
|
impl Widget for MyList {
|
|
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
|
|
while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
|
|
if let Some(mut list) = item.borrow_mut::<PortalList>() {
|
|
list.set_item_range(cx, 0, 100); // 100 items
|
|
|
|
while let Some(item_id) = list.next_visible_item(cx) {
|
|
let item = list.item(cx, item_id, id!(Item));
|
|
item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id));
|
|
item.draw_all(cx, &mut Scope::empty());
|
|
}
|
|
}
|
|
}
|
|
DrawStep::done()
|
|
}
|
|
}
|
|
```
|
|
|
|
### FileTree Usage
|
|
|
|
```rust
|
|
impl Widget for FileTreeDemo {
|
|
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
|
|
while self.file_tree.draw_walk(cx, scope, walk).is_step() {
|
|
self.file_tree.set_folder_is_open(cx, live_id!(root), true, Animate::No);
|
|
// Draw nodes recursively
|
|
self.draw_node(cx, live_id!(root));
|
|
}
|
|
DrawStep::done()
|
|
}
|
|
}
|
|
```
|
|
|
|
### Registering Custom Draw Shaders
|
|
|
|
For custom draw types with shader fields, use `script_shader`:
|
|
|
|
```rust
|
|
script_mod!{
|
|
use mod.prelude.widgets_internal.*
|
|
|
|
// Register custom draw shader
|
|
set_type_default() do #(DrawMyShader::script_shader(vm)){
|
|
..mod.draw.DrawQuad // Inherit from DrawQuad
|
|
}
|
|
|
|
// Register widget that uses it
|
|
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
|
|
}
|
|
|
|
#[derive(Script, ScriptHook)]
|
|
#[repr(C)]
|
|
struct DrawMyShader {
|
|
#[deref] draw_super: DrawQuad,
|
|
#[live] my_param: f32,
|
|
}
|
|
```
|
|
|
|
### Registering Components (non-Widget)
|
|
|
|
For structs that aren't full widgets but need script registration:
|
|
|
|
```rust
|
|
script_mod!{
|
|
// For components (not widgets)
|
|
mod.widgets.MyComponentBase = #(MyComponent::script_component(vm))
|
|
|
|
// For widgets (implements Widget trait)
|
|
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
|
|
}
|
|
```
|
|
|
|
### Script Prelude Modules
|
|
|
|
Two prelude modules available:
|
|
- `mod.prelude.widgets_internal.*` - For internal widget library development
|
|
- `mod.prelude.widgets.*` - For app development (includes all widgets)
|
|
|
|
```rust
|
|
script_mod!{
|
|
// App development - use widgets prelude
|
|
use mod.prelude.widgets.*
|
|
|
|
// Or for widget library internals
|
|
use mod.prelude.widgets_internal.*
|
|
use mod.widgets.*
|
|
}
|
|
```
|
|
|
|
### Default Enum Values
|
|
|
|
For enums with a `None` variant that need `Default`, use standard Rust `#[default]` attribute instead of `DefaultNone` derive:
|
|
|
|
```rust
|
|
// Correct - use #[default] attribute on the None variant
|
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
|
pub enum MyAction {
|
|
SomeAction,
|
|
AnotherAction,
|
|
#[default]
|
|
None,
|
|
}
|
|
|
|
// Wrong - don't use DefaultNone derive
|
|
#[derive(Clone, Copy, Debug, PartialEq, DefaultNone)] // Don't do this
|
|
pub enum MyAction {
|
|
SomeAction,
|
|
None,
|
|
}
|
|
```
|
|
|
|
### Multi-Module Script Registration Pattern
|
|
|
|
When refactoring a multi-file project (like studio) from `live_design!` to `script_mod!`:
|
|
|
|
1. **Each widget module** defines its own `script_mod!` that registers to `mod.widgets.*`:
|
|
```rust
|
|
// In studio_editor.rs
|
|
script_mod! {
|
|
use mod.prelude.widgets_internal.*
|
|
use mod.widgets.*
|
|
|
|
mod.widgets.StudioCodeEditorBase = #(StudioCodeEditor::register_widget(vm))
|
|
mod.widgets.StudioCodeEditor = set_type_default() do mod.widgets.StudioCodeEditorBase {
|
|
editor := CodeEditor {}
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **The lib.rs** aggregates all widget script_mods:
|
|
```rust
|
|
pub fn script_mod(vm: &mut ScriptVm) {
|
|
crate::module1::script_mod(vm);
|
|
crate::module2::script_mod(vm);
|
|
// ... all widget modules
|
|
}
|
|
```
|
|
|
|
3. **The app.rs** calls them in correct order:
|
|
```rust
|
|
impl App {
|
|
fn run(vm: &mut ScriptVm) -> Self {
|
|
crate::makepad_widgets::script_mod(vm); // Base widgets first
|
|
crate::script_mod(vm); // Your widget modules
|
|
crate::app_ui::script_mod(vm); // UI that uses the widgets
|
|
App::from_script_mod(vm, self::script_mod)
|
|
}
|
|
}
|
|
```
|
|
|
|
4. **The app_ui.rs** can then use registered widgets:
|
|
```rust
|
|
script_mod! {
|
|
use mod.prelude.widgets.*
|
|
// Now StudioCodeEditor is available from mod.widgets
|
|
|
|
let EditorContent = View {
|
|
editor := StudioCodeEditor {}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Cross-Module Sharing via `mod` Object
|
|
|
|
**IMPORTANT**: `use crate.module.*` does NOT work in script_mod. The `crate.` prefix is not available.
|
|
|
|
To share definitions between script_mod blocks in different files, store them in the `mod` object:
|
|
|
|
```rust
|
|
// In app_ui.rs - export to mod.widgets namespace
|
|
script_mod! {
|
|
use mod.prelude.widgets.*
|
|
|
|
// This makes AppUI available as mod.widgets.AppUI
|
|
mod.widgets.AppUI = Window{
|
|
// ...
|
|
}
|
|
}
|
|
|
|
// In app.rs - import via mod.widgets
|
|
script_mod! {
|
|
use mod.prelude.widgets.*
|
|
use mod.widgets.* // Now AppUI is in scope
|
|
|
|
load_all_resources() do #(App::script_component(vm)){
|
|
ui: Root{ AppUI{} }
|
|
}
|
|
}
|
|
```
|
|
|
|
The `mod` object is the only way to share data between script_mod blocks.
|
|
|
|
### Prelude Alias Syntax
|
|
|
|
When defining a prelude, use `name:mod.path` to create an alias:
|
|
```rust
|
|
mod.prelude.widgets = {
|
|
..mod.std, // Spread all of mod.std into scope
|
|
theme:mod.theme, // Create 'theme' as alias for mod.theme
|
|
draw:mod.draw, // Create 'draw' as alias for mod.draw
|
|
}
|
|
```
|
|
|
|
Without the alias (just `mod.theme,`), the module is included but has no name - you can't access it!
|
|
|
|
### Let Bindings are Local
|
|
|
|
`let` bindings in script_mod are LOCAL to that script_mod block. They cannot be:
|
|
- Accessed from other script_mod blocks
|
|
- Used as property values directly (e.g., `content +: MyLetBinding` won't work)
|
|
|
|
To use a `let` binding, instantiate it: `MyLetBinding{}` or store it in `mod.*` for cross-module access.
|
|
|
|
### Debug Logging with `~`
|
|
|
|
Use `~expression` to log the value of an expression during script evaluation:
|
|
```rust
|
|
script_mod! {
|
|
~mod.theme // Logs the theme object
|
|
~mod.prelude.widgets // Logs what's in the prelude
|
|
~some_variable // Logs a variable's value (or "not found" error)
|
|
}
|
|
```
|
|
|
|
### Common Pitfalls
|
|
|
|
**Widget ID references**: Named widget instances use `:=` in the DSL and plain names in Rust id macros:
|
|
- DSL defines `code_block := View { ... }` → Rust uses `id!(code_block)`
|
|
- DSL defines `my_button := Button { ... }` → Rust uses `ids!(my_button)`
|
|
|
|
1. **Missing `#[source]`**: All Script-derived structs need `#[source] source: ScriptObjectRef`
|
|
|
|
2. **Template scope**: Templates defined inside Dock aren't available outside; use `let` at script level
|
|
|
|
3. **Uniform vs Instance**: Use `instance()` for per-widget varying colors (like hover states on backgrounds)
|
|
|
|
4. **Forgot `+:`**: Without `+:`, you replace the entire property instead of merging
|
|
|
|
5. **Theme access**: Always `theme.color_x`, never `THEME_COLOR_X` or `(theme.color_x)`
|
|
|
|
6. **Missing widget registration**: Call `crate::makepad_widgets::script_mod(vm)` in `App::run()` before your own `script_mod`. Note: the old `live_design!` system and its crates are archived under `old/`
|
|
|
|
7. **Draw shader repr**: Custom draw shaders need `#[repr(C)]` for correct memory layout
|
|
|
|
8. **DefaultNone derive**: Don't use `DefaultNone` derive - use standard `#[derive(Default)]` with `#[default]` attribute on the `None` variant
|
|
|
|
9. **Script_mod call order**: Widget modules must be registered BEFORE UI modules that use them. Always call `lib.rs::script_mod` before `app_ui::script_mod`
|
|
|
|
10. **`pub` keyword invalid in script_mod**: Don't use `pub mod.widgets.X = ...`, just use `mod.widgets.X = ...`. Visibility is controlled by the Rust module system, not script_mod.
|
|
|
|
11. **Syntax for Inset/Align/Walk**: Use constructor syntax - `margin: Inset{left: 10}` not `margin: {left: 10}`, `align: Align{x: 0.5 y: 0.5}` not `align: {x: 0.5, y: 0.5}`
|
|
|
|
12. **Cursor values**: Use `cursor: MouseCursor.Hand` not `cursor: Hand` or `cursor: @Hand`
|
|
|
|
13. **Resource paths**: Use `crate_resource("self://path")` not `dep("crate://self/path")`
|
|
|
|
14. **Texture declarations in shaders**: Use `tex: texture_2d(float)` not `tex: texture2d`
|
|
|
|
15. **Enums not exposed to script**: Some Rust enums like `PopupMenuPosition::BelowInput` may not be exposed to script. If you get "not found" errors on enum variants, just remove the property and use the default
|
|
|
|
17. **Shader `mod` vs `modf`**: The Makepad shader language uses `modf(a, b)` for float modulo, NOT `mod(a, b)`. Similarly, use `atan2(y, x)` not `atan(y, x)` for two-argument arctangent. `atan(x)` (single arg) is also available. `fract(x)` works as expected.
|
|
|
|
16. **Draw shader struct field ordering**: In `#[repr(C)]` draw shader structs that extend another draw shader via `#[deref]`, NEVER place `#[rust]` or other non-instance data AFTER `DrawVars` and the instance fields. The system uses an unsafe pointer trick in `DrawVars::as_slice()` that reads contiguously past the end of `dyn_instances` into the subsequent `#[live]` fields. Any non-instance data between `DrawVars` and the instance fields will corrupt the GPU instance buffer. Put all extra data (like `#[rust]`, `#[live]` non-instance fields such as resource handles, booleans, etc.) BEFORE the `#[deref]` field, and only `#[live]` instance fields (the ones that map to shader inputs) AFTER.
|
|
```rust
|
|
// CORRECT - non-instance data before deref, instance fields after
|
|
#[derive(Script, ScriptHook)]
|
|
#[repr(C)]
|
|
pub struct MyDrawShader {
|
|
#[live] pub svg: Option<ScriptHandleRef>, // non-instance, BEFORE deref
|
|
#[rust] my_state: bool, // non-instance, BEFORE deref
|
|
#[deref] pub draw_super: DrawVector, // contains DrawVars + base instance fields
|
|
#[live] pub tint: Vec4f, // instance field, AFTER deref - OK
|
|
}
|
|
|
|
// WRONG - rust data after instance fields breaks the memory layout
|
|
#[derive(Script, ScriptHook)]
|
|
#[repr(C)]
|
|
pub struct MyDrawShader {
|
|
#[deref] pub draw_super: DrawVector,
|
|
#[live] pub tint: Vec4f, // instance field
|
|
#[rust] my_state: bool, // BAD: sits between tint and the next shader's fields
|
|
}
|
|
```
|
|
|
|
18. **Don't put comments or blank lines before the first real code in `script!`/`script_mod!`**: Rust's proc macro token stream strips comments entirely — they produce no tokens. This shifts error column/line info because the span tracking starts from the first actual token. Always start with real code (e.g., `use mod.std.assert`) immediately after the opening brace.
|
|
|
|
19. **WARNING: Hex colors containing the letter `e` in `script_mod!`**: The Rust tokenizer interprets `e` or `E` in hex color literals as a scientific notation exponent, causing parse errors like `expected at least one digit in exponent`. For example, `#2ecc71` fails because `2e` looks like the start of `2e<exponent>`. **Use the `#x` prefix** to escape this: write `#x2ecc71` instead of `#x2ecc71`. This applies to any hex color where a digit is immediately followed by `e`/`E` (e.g., `#1e1e2e`, `#4466ee`, `#7799ee`, `#bb99ee`). Colors without `e` (like `#ff4444`, `#44cc44`) work fine with plain `#`.
|
|
|
|
20. **Shader enums**: Prefer `match` on enum values with `_ =>` as the catch-all arm, not `if/else` chains over integer-like values. If enum `match` fails in shader compilation, treat it as a compiler bug: add or extend a `platform/script/test` case and fix the shader compiler path instead of rewriting shader logic to `if/else`.
|