mcpField
M
makepad-2.0-app-structure

makepad-2.0-app-structure

github.com/zhanghandong开发75254 次安装29 周前更新
代码仓库

关于 makepad-2.0-app-structure

面向 Makepad 2.0 开发者的技能指南,介绍 Rust 与 Splash 的应用结构、通信和事件处理,并涵盖运行、调试及音频和系统集成模式。

主要功能

  1. 01Rust 与 Splash 应用结构
  2. 02Rust 和 Splash 双向通信
  3. 03事件路由与控件访问
  4. 04运行和热重载方式
  5. 05音频处理与系统集成

适用场景

  1. 01搭建 Makepad 2.0 应用骨架
  2. 02在 Rust 与 Splash 间同步状态
  3. 03通过 Makepad Studio 调试 Splash
  4. 04实现音频输入输出或系统快捷键

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zhanghandong/makepad-skills --skill makepad-2.0-app-structure

安装后在 Claude Code 中输入/makepad-2.0-app-structure确认已加载。

附带文件references/app-boilerplate.mdreferences/rust-splash-integration.md

详细介绍

Makepad 2.0 应用结构技能

版本: makepad-widgets(dev 分支)| 最后更新: 2026-03-03

概述

Makepad 2.0 应用将 Rust 代码与 Splash 脚本结合使用。Rust 端负责应用生命周期、事件路由和业务逻辑。Splash 端定义 UI 结构、模板和内联交互。

文档

详细文档请参阅本地文件:

  • ./references/app-boilerplate.md - 完整的可运行应用模板,包含 Cargo.toml
  • ./references/rust-splash-integration.md - Rust ↔ Splash 通信模式

重要:检查文档完整性

回答问题前,Claude 必须:

  1. 阅读上方列出的相关参考文件
  2. 如果读取文件失败,请根据 SKILL.md 中的模式和内置知识作答

最简应用模板

Cargo.toml
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"

[dependencies]
makepad-widgets = { path = "../path/to/makepad/widgets" }
src/app.rs(或 src/main.rs)
use makepad_widgets::*;

app_main!(App);

script_mod! {
    use mod.prelude.widgets.*

    let state = {
        counter: 0
    }
    mod.state = state

    startup() do #(App::script_component(vm)){
        ui: Root{
            on_startup: ||{
                ui.main_view.render()
            }
            main_window := Window{
                window.inner_size: vec2(420, 300)
                body +: {
                    main_view := View{
                        width: Fill height: Fill
                        flow: Down spacing: 12
                        align: Center
                        on_render: ||{
                            Label{
                                text: "Count: " + state.counter
                                draw_text.text_style.font_size: 24
                            }
                        }
                    }
                    increment_button := Button{
                        text: "Increment"
                    }
                }
            }
        }
    }
}

impl App {
    fn run(vm: &mut ScriptVm) -> Self {
        crate::makepad_widgets::script_mod(vm);
        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(cx, ids!(increment_button)).clicked(actions) {
            script_eval!(cx, {
                mod.state.counter += 1
                ui.main_view.render()
            });
        }
    }
}

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());
    }
}

关键组件说明

1. app_main!(App)

将 App 注册为应用入口点。必须在模块级调用。

2. script_mod! { ... }

定义启动时运行的 Splash 脚本。包含:

  • use mod.prelude.widgets.* - 导入控件定义
  • 状态定义(let state = {...})
  • 函数和模板
  • startup() do #(App::script_component(vm)){...} - 构建 UI
3. App::run(vm) - 初始化顺序

关键:注册顺序很重要!

fn run(vm: &mut ScriptVm) -> Self {
    // 1. Register theme (optional, for light/dark theme)
    crate::makepad_widgets::theme_mod(vm);
    script_eval!(vm, { mod.theme = mod.themes.light });

    // 2. Register base widgets
    crate::makepad_widgets::widgets_mod(vm);

    // 3. Register custom widget modules (if any)
    // crate::my_widgets::script_mod(vm);

    // 4. Build app from script module
    App::from_script_mod(vm, self::script_mod)
}

简化版(不选择主题):

fn run(vm: &mut ScriptVm) -> Self {
    crate::makepad_widgets::script_mod(vm);   // Registers both theme + widgets
    App::from_script_mod(vm, self::script_mod)
}
4. App 结构体上的 #[derive(Script, ScriptHook)]
#[derive(Script, ScriptHook)]
pub struct App {
    #[live]
    ui: WidgetRef,    // The widget tree root
}
  • Script - 启用 Splash 集成(替代旧版 Live)
  • ScriptHook - 启用生命周期钩子(替代旧版 LiveHook)
  • #[live] - 可通过 DSL 设置的字段
5. 用于处理操作的 MatchEvent
impl MatchEvent for App {
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        // Check button clicks
        if self.ui.button(cx, ids!(my_button)).clicked(actions) {
            // Handle click
        }
        // Check text input changes
        if let Some(text) = self.ui.text_input(cx, ids!(my_input)).changed(actions) {
            log!("Input: {}", text);
        }
    }
}
6. 用于事件分发的 AppMain
impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.match_event(cx, event);  // MUST call for MatchEvent to work
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

Rust → Splash 通信

script_eval! - 更新状态并触发渲染
script_eval!(cx, {
    mod.state.counter += 1
    ui.main_view.render()
});
script_apply_eval! - 修补控件属性
script_apply_eval!(cx, self.ui, {
    title.text: "New Title"
    subtitle.draw_text.color: #f00
});

Splash → Rust 通信

script_mod! 中的内联事件处理器
Button{
    text: "Click"
    on_click: || {
        // Splash code runs here
        state.count += 1
        ui.display.render()
    }
}

TextInput{
    on_return: || {
        let text = ui.my_input.text()
        add_item(text)
        ui.my_input.set_text("")
    }
}

// Render callback
on_render: || {
    for i, item in items {
        ItemTemplate{label.text: item.name}
    }
}

从 Rust 访问控件

// Buttons
self.ui.button(cx, ids!(button_name)).clicked(actions)

// Labels
self.ui.label(cx, ids!(label_name)).set_text(cx, "text")

// Text inputs
self.ui.text_input(cx, ids!(input_name)).text()

// Nested access
self.ui.label(cx, ids!(container.inner.title))

运行应用

# Development
cargo run -p my-app

# Development with hot reload (Splash changes apply without recompilation)
cargo run -p my-app -- --hot

# Release
cargo run -p my-app --release

# With cargo-makepad for mobile/web
cargo makepad run -p my-app
命令行标志
标志说明
--hot启用热重载:监视 script_mod! 源文件,并在保存时自动刷新 UI。仅影响 Splash DSL;Rust 代码变更仍需重新编译。
--stdin-loopStudio 模式:通过 stdin/websocket 与 Makepad Studio 通信。供 Studio 内部使用,不适用于手动操作。
--linux-backend=<x11|wayland>(仅限 Linux)选择窗口后端。

Makepad Studio:调试 Splash

在 Studio 中运行 Splash 脚本

Studio 会在项目根目录查找 makepad.splash 文件。它可以通过 运行列表 面板直接运行 Splash 脚本(内部使用 start_script_run),无需执行 cargo run。

调试输出

在 Splash 脚本中使用 std.println() / std.print(),输出会同时显示在 Studio 的 日志视图 面板和终端 stdout 中:

// makepad.splash
std.println("debug: value = " + my_var);
错误显示

脚本运行失败时,Studio 的日志视图会显示文件路径和错误详情:

script failed while evaluating makepad.splash: <error details>
Hub API(由 Studio 注入的 hub 模块)

在 Studio 下运行时,Splash 脚本会获得一个 hub 模块:

API说明
hub.run(env, cmd, args)从 Splash 启动子进程
hub.set_run_items(items)在 Studio 的运行列表中注册可运行项目
hub.studio_ipStudio 的 WebSocket 地址
当前限制
  • 不支持断点调试 — Splash VM 不支持断点或单步执行
  • 没有 AST 转储标志 — 检查解析结果需要在 Rust 源代码(script/)中添加日志
  • 基于打印的调试 — std.println() 是主要调试工具

音频 API(仅限 Rust,未向 Splash 开放)

Makepad 2.0 通过 CxMediaApi trait 提供原生音频 I/O。音频回调在独立的实时线程上运行。

音频输出
use makepad_widgets::*;

impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        match event {
            Event::Startup => {
                cx.audio_output(0, move |_info, buffer: &mut AudioBuffer| {
                    for frame in 0..buffer.frame_count() {
                        let sample = /* generate sample */;
                        buffer.set(0, frame, sample);
                        buffer.set(1, frame, sample);
                    }
                });
            }
            _ => {}
        }
        self.match_event(cx, event);
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}
音频输入
cx.audio_input(0, move |info: AudioInfo, buffer: &AudioBuffer| {
    let sample_rate = info.sample_rate;
    for frame in 0..buffer.frame_count() {
        let sample = buffer.get(0, frame);
    }
});
主要音频类型(platform/src/audio.rs)
类型描述
AudioBuffer多声道采样容器(f32)
AudioInfo设备 ID、采样率、计时信息
AudioDeviceDesc设备元数据(名称、类型、声道数)
AudioStreamSender/Receiver采用自适应缓冲的解耦音频路由
跨线程数据流模式(音频 → UI)

音频回调在实时线程上运行。使用原子变量将数据传递到 UI 线程:

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

let amplitude = Arc::new(AtomicU64::new(0));
let amp_clone = amplitude.clone();

cx.audio_output(0, move |_info, buffer: &mut AudioBuffer| {
    let mut sum = 0.0f32;
    for frame in 0..buffer.frame_count() {
        let s = buffer.get(0, frame);
        sum += s * s;
    }
    let rms = (sum / buffer.frame_count() as f32).sqrt();
    amp_clone.store(rms.to_bits() as u64, Ordering::Relaxed);
});

// UI thread: read on NextFrame
let rms = f32::from_bits(amplitude.load(Ordering::Relaxed) as u32);

重要: Splash 脚本无法访问音频。所有音频处理都必须在 Rust 中完成。

平台支持
平台音频输出音频输入实现
macOS/iOSAudioUnitAVCaptureapple_media.rs
WindowsWASAPIWASAPIwindows_media.rs
LinuxALSA/PulseAudioALSAlinux_media.rs
Web/WASMWebAudioMediaDevicesweb_media.rs
AndroidAAudioNDK Camera2android_media.rs
参考:TeamTalk 示例

examples/teamtalk/ 演示了完整的 P2P 音频聊天,包括 cx.audio_input()/cx.audio_output()、重采样和 UDP 流传输。


最佳实践

  1. 注册顺序——主题 → 基础组件 → 自定义组件 → 应用模块
  2. 使用 script_eval! 将 Rust 操作桥接到 Splash 状态更新
  3. 在 handle_event 中调用 self.match_event(cx, event)(MatchEvent 必需)
  4. 动态内容使用 on_render,调用 .render() 触发更新
  5. 业务逻辑放在 Rust 中,UI 声明放在 Splash 中
  6. 使用 mod.state 管理 Rust 和 Splash 均可访问的应用级状态
  7. 仅在 Rust 中进行音频处理——使用原子变量实现音频 → UI 数据流

macOS 系统集成模式(学习于 2026-03-31)

NSStatusBar(菜单栏图标)

Makepad 没有内置的 NSStatusBar API。使用 makepad_objc_sys(与 Makepad 使用相同的 ObjC 运行时)进行系统托盘集成:

use makepad_objc_sys::{msg_send, class, sel, sel_impl};
use makepad_objc_sys::runtime::{Object, Sel, YES, NO};

// Create status bar item
let status_bar: ObjcId = msg_send![class!(NSStatusBar), systemStatusBar];
let item: ObjcId = msg_send![status_bar, statusItemWithLength: -1.0f64];
let button: ObjcId = msg_send![item, button];
let () = msg_send![button, setTitle: str_to_nsstring("MIC")];

关键注意事项:

  • 不要使用 objc2 crate——它会创建独立的 NSApplication 实例,与 Makepad 冲突
  • show_in_dock(false) 会隐藏 NSStatusItem——改为在 Info.plist 中设置 LSUIElement=true
  • 菜单目标对象会被自动释放——使用全局单例目标和 sender.tag() 分发操作
  • 不要调用 CGEventTapEnable(tap, true)——创建 CGEvent tap 时默认已启用;调用 Enable 实际上会禁用它们
音频输入——必须提供设备 ID

cx.use_audio_inputs(&[]) 表示停止录音,而不是“使用默认设备”。必须从 AudioDevicesEvent 获取设备 ID:

impl MatchEvent for App {
    fn handle_audio_devices(&mut self, _cx: &mut Cx, e: &AudioDevicesEvent) {
        // Save default device ID
        self.default_input = e.default_input().into_iter().next();
    }
}

// When starting recording:
if let Some(device_id) = self.default_input {
    cx.use_audio_inputs(&[device_id]);  // Start capture with specific device
}

// When stopping:
cx.use_audio_inputs(&[]);  // Empty = stop all audio input
CGEvent Tap——全局快捷键

用于全局键盘快捷键(例如,按下即可说话):

// CGEventGetFlags returns 0x80000 for Option key (not 0x20 as some docs suggest)
let key_mask: u64 = 0x080000; // kCGEventFlagMaskAlternate

// Use CFRunLoopCommonModes (not DefaultMode)
CFRunLoopAddSource(run_loop, source, kCFRunLoopCommonModes);

// Do NOT call CGEventTapEnable — it's already enabled
// CGEventTapEnable(tap, true);  // WRONG — this actually breaks it

// CFRunLoopRun blocks forever — put in dedicated thread
CFRunLoopRun();
跨线程通信模式
macos-sys (CFRunLoop thread) → crossbeam channel → Makepad timer poll (main thread)
Audio callback (RT thread)   → Arc<AtomicU64>    → NextFrame handler (main thread)
HTTP response               → MatchEvent handler → UI update via script_eval!

用于接收 ObjC 回调的定时器轮询模式:

// In handle_timer (10ms interval):
let events: Vec<u64> = self.menu_rx.as_ref()
    .map(|rx| rx.try_iter().collect())
    .unwrap_or_default();
for action_id in events {
    self.handle_menu_action(cx, action_id);
}

评论

支持 Markdown