mcpField
M
makepad-2.0-app-structure

makepad-2.0-app-structure

github.com/zhanghandong開発75254 回インストール29 週間前に更新
リポジトリ

makepad-2.0-app-structure について

Rust と Splash で Makepad 2.0 アプリを開発する人向けのスキルガイドです。アプリ構成、相互通信、イベント処理、実行・デバッグに加え、オーディオやシステム連携のパターンを扱います。

主な機能

  1. 01Rust と Splash のアプリ構成
  2. 02Rust と Splash の双方向通信
  3. 03イベント処理とウィジェット操作
  4. 04実行とホットリロード
  5. 05オーディオとシステム連携

活用シーン

  1. 01Makepad 2.0 のアプリ骨格を作成
  2. 02Rust と Splash 間の状態同期
  3. 03Makepad 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 ファイルを探します。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_ipStudio の 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/iOSAudioUnitAVCaptureapple_media.rs
WindowsWASAPIWASAPIwindows_media.rs
LinuxALSA/PulseAudioALSAlinux_media.rs
Web/WASMWebAudioMediaDevicesweb_media.rs
AndroidAAudioNDK Camera2android_media.rs
参考:TeamTalk の例

examples/teamtalk/ では、cx.audio_input()/cx.audio_output()、リサンプリング、UDP ストリーミングを使った、完全な P2P オーディオチャットを紹介しています。


ベストプラクティス

  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 クレートは使用しないでください — 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);
}

コメント