makepad-2.0-app-structure について
Rust と Splash で Makepad 2.0 アプリを開発する人向けのスキルガイドです。アプリ構成、相互通信、イベント処理、実行・デバッグに加え、オーディオやシステム連携のパターンを扱います。
主な機能
- 01Rust と Splash のアプリ構成
- 02Rust と Splash の双方向通信
- 03イベント処理とウィジェット操作
- 04実行とホットリロード
- 05オーディオとシステム連携
活用シーン
- 01Makepad 2.0 のアプリ骨格を作成
- 02Rust と Splash 間の状態同期
- 03Makepad Studio で Splash をデバッグ
- 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 は必ず次のことを行ってください。
- 上記の関連するリファレンスファイルを読む
- ファイルを読み取れない場合は、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-loop | Studio モード:stdin/websocket 経由で Makepad Studio と通信します。Studio 内部で使用され、手動での使用を目的としていません。 |
--linux-backend=<x11|wayland> | (Linux のみ)ウィンドウバックエンドを選択します。 |
Makepad Studio:Splash のデバッグ
Studio で Splash スクリプトを実行する
Studio はプロジェクトのルートにある makepad.splash ファイルを探します。Run List パネルから Splash スクリプトを直接実行でき(内部では start_script_run を使用)、cargo run は必要ありません。
デバッグ出力
Splash スクリプト内で std.println() / std.print() を使用すると、Studio の Log View パネルとターミナルの stdout の両方に出力されます。
// makepad.splash
std.println("debug: value = " + my_var);
エラー表示
スクリプトが失敗すると、ファイルパスとエラーの詳細が Studio の Log View に表示されます。
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 の Run List に実行可能な項目を登録します |
hub.studio_ip | Studio の WebSocket アドレス |
現在の制限事項
- ブレークポイントデバッグなし — Splash VM はブレークポイントやステップ実行をサポートしていません
- AST ダンプフラグなし — 解析結果の確認には、Rust ソース(
script/)にログを追加する必要があります - プリントベースのデバッグ —
std.println()が主なデバッグツールです
オーディオ API(Rust 専用、Splash には公開されていません)
Makepad 2.0 は、CxMediaApi トレイトを介してネイティブのオーディオ 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/iOS | AudioUnit | AVCapture | apple_media.rs |
| Windows | WASAPI | WASAPI | windows_media.rs |
| Linux | ALSA/PulseAudio | ALSA | linux_media.rs |
| Web/WASM | WebAudio | MediaDevices | web_media.rs |
| Android | AAudio | NDK Camera2 | android_media.rs |
参考:TeamTalk の例
examples/teamtalk/ では、cx.audio_input()/cx.audio_output()、リサンプリング、UDP ストリーミングを使った、完全な P2P オーディオチャットを紹介しています。
ベストプラクティス
- 登録順序 — テーマ → 基本ウィジェット → カスタムウィジェット → アプリモジュール
script_eval!を使用すると、Rust のアクションを Splash の状態更新につなげられます- handle_event で
self.match_event(cx, event)を呼び出す(MatchEvent に必須) - 動的コンテンツには
on_renderを使用する。更新をトリガーするには.render()を呼び出します - ビジネスロジックは Rust に、UI 宣言は Splash に置く
mod.stateを使用すると、Rust と Splash の両方からアクセスできるアプリ全体の状態を管理できます- オーディオ処理は 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クレートは使用しないでください — Makepad のものと競合する別の NSApplication インスタンスが作成されますshow_in_dock(false)は NSStatusItem を非表示にします — 代わりに Info.plist でLSUIElement=trueを使用してください- メニューのターゲットオブジェクトは自動解放されます — グローバルシングルトンのターゲットと
sender.tag()を使用してアクションをディスパッチしてください CGEventTapEnable(tap, true)は呼び出さないでください — CGEvent タップは作成時にデフォルトで有効になっており、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);
}