mcpField
M
makepad-2.0-events

makepad-2.0-events

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

makepad-2.0-events について

Makepad 2.0 アプリ開発者向けのイベントシステム解説スキルです。Splash のハンドラーと Rust のイベント処理の連携、および UI や状態をつなぐ仕組みを説明します。

主な機能

  1. 01Splash のインラインイベントハンドラー
  2. 02Rust MatchEvent によるイベント処理
  3. 03ウィジェットのアクションと参照 API
  4. 04Rust と Splash の連携マクロ
  5. 05タイマーと HTTP イベント処理

活用シーン

  1. 01クリックやフォーム送信の処理
  2. 02Rust から状態を更新して再描画
  3. 03アプリコードからウィジェットのアクションを取得
  4. 04タイマーや HTTP 応答処理の追加

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/makepad-2.0-eventsを入力して読み込まれたか確認します。

同梱ファイルreferences/event-patterns.md

詳細

Makepad 2.0 イベントとアクションのシステム

概要

Makepad 2.0 は2 層のイベントシステムを使用します。

  1. Splash レイヤー -- script_mod! の Splash コード内に直接記述するインラインイベントハンドラー (on_click、on_render、on_return、on_startup)。スクリプト内でウィジェット定義の近くに宣言的に記述し、UI 操作を処理します。

  2. Rust レイヤー -- MatchEvent トレイトと handle_actions、handle_timer、 handle_http_response など。ビジネスロジック、外部 I/O、および Rust の機能をフルに必要とする処理を担当します。

両レイヤーは 2 つのブリッジマクロを介して連携します。

  • script_eval!(cx, { ... }) -- Rust から Splash コードを実行します(状態の更新、レンダリングのトリガー)
  • script_apply_eval!(cx, widget_ref, { ... }) -- 実行時に Rust からウィジェットのプロパティを更新します

1. Splash インラインイベントハンドラー

イベントハンドラーは script_mod! ブロック内でウィジェットに直接設定します。引数なしの場合は ||、値を受け取るコールバックの場合は |arg| を使うクロージャー構文を使用します。

on_click -- ボタン/ウィジェットのクリック

ユーザーがボタンまたはクリック可能なウィジェットをクリックすると呼び出されます。通常のボタンには引数がありません。CheckBox では |checked| を指定し、新しいブール値の状態を受け取ります。

// Plain button click
add_button := Button{
    text: "Add"
    on_click: ||{
        let text = ui.todo_input.text()
        if text != "" {
            add_todo(text, "")
            ui.todo_input.set_text("")
        }
    }
}

// CheckBox click with checked state argument
check.on_click: |checked| toggle_todo(i, checked)

// Inline delete with closure capturing loop variable
delete.on_click: || delete_todo(i)

// Calling another widget's click programmatically
clear_done := ButtonFlatter{
    text: "Clear completed"
    on_click: ||{
        todos.retain(|todo| !todo.done)
        ui.todo_list.render()
    }
}
on_render -- 動的レンダリング

対象のビューで .render() が呼び出されると実行されます。動的コンテンツを表示するための主要な仕組みです。本文の内容がビューの以前の描画内容に置き換わります。

main_view := View{
    width: Fill
    height: Fill
    on_render: ||{
        counter_label := Label{
            text: "Count: " + state.counter
            draw_text.text_style.font_size: 24
        }
    }
}

// List rendering with for loop and per-item event handlers
todo_list := ScrollYView{
    width: Fill height: Fill
    new_batch: true
    on_render: ||{
        if todos.len() == 0
            EmptyState{}
        else for i, todo in todos {
            TodoItem{
                label.text: todo.text
                check.active: todo.done
                check.on_click: |checked| toggle_todo(i, checked)
                delete.on_click: || delete_todo(i)
            }
        }
    }
    EmptyState{}
}

重要:on_render は自動的には呼び出されません。実行するには ui.widget_name.render() を呼び出す必要があります。ビューの new_batch: true プロパティを指定すると、再レンダリング前に以前の描画内容がクリアされます。

on_return -- TextInput の Enter キー

TextInput 内でユーザーが Enter/Return キーを押すと実行されます。フォームの送信によく使用します。

todo_input := TextInput{
    width: Fill height: 9. * theme.space_1
    empty_text: "What needs to be done?"
    on_return: || ui.add_button.on_click()
}
on_startup -- アプリの起動

アプリケーションの起動時に一度だけ実行されます。Root レベルで定義します。初期レンダリングのトリガーによく使用します。

ui: Root{
    on_startup: ||{
        ui.main_view.render()
    }
    main_window := Window{
        // ...
    }
}
イベントハンドラーでできること

イベントハンドラー内では、次の操作ができます。

  • Splash 関数の呼び出し:add_todo(text, "dev")
  • ウィジェットの値の読み取り:let text = ui.todo_input.text()
  • ウィジェットの値の設定:ui.todo_input.set_text("")
  • 再レンダリングのトリガー:ui.todo_list.render()
  • 別のウィジェットのクリックのトリガー:ui.add_button.on_click()
  • 状態変数の変更:state.counter += 1
  • 配列メソッドの使用:todos.push({text: "new", done: false})
  • 制御フローの使用:if text != "" { ... }

2. Rust のイベント処理 -- MatchEvent トレイト

MatchEvent トレイトは Rust 側のイベントディスパッチャーです。ハンドラーメソッド群を通じて、プラットフォームイベントとウィジェットアクションを受け取ります。

コアトレイトの定義(draw/src/match_event.rs より)
pub trait MatchEvent {
    // Lifecycle
    fn handle_startup(&mut self, _cx: &mut Cx) {}
    fn handle_shutdown(&mut self, _cx: &mut Cx) {}
    fn handle_foreground(&mut self, _cx: &mut Cx) {}
    fn handle_background(&mut self, _cx: &mut Cx) {}
    fn handle_pause(&mut self, _cx: &mut Cx) {}
    fn handle_resume(&mut self, _cx: &mut Cx) {}

    // Window focus
    fn handle_window_got_focus(&mut self, _cx: &mut Cx, _window_id: &WindowId) {}
    fn handle_window_lost_focus(&mut self, _cx: &mut Cx, _window_id: &WindowId) {}

    // Frame
    fn handle_next_frame(&mut self, _cx: &mut Cx, _e: &NextFrameEvent) {}

    // Widget actions (most commonly used)
    fn handle_action(&mut self, _cx: &mut Cx, _e: &Action) {}
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        for action in actions {
            self.handle_action(cx, action);
        }
    }

    // Input
    fn handle_key_down(&mut self, _cx: &mut Cx, _e: &KeyEvent) {}
    fn handle_key_up(&mut self, _cx: &mut Cx, _e: &KeyEvent) {}
    fn handle_back_pressed(&mut self, _cx: &mut Cx) -> bool { false }

    // Timer
    fn handle_timer(&mut self, _cx: &mut Cx, _e: &TimerEvent) {}

    // Drawing
    fn handle_draw(&mut self, _cx: &mut Cx, _e: &DrawEvent) {}
    fn handle_draw_2d(&mut self, _cx: &mut Cx2d) {}

    // Network
    fn handle_http_response(&mut self, _cx: &mut Cx, _request_id: LiveId, _response: &HttpResponse) {}
    fn handle_http_request_error(&mut self, _cx: &mut Cx, _request_id: LiveId, _err: &HttpError) {}
    fn handle_http_progress(&mut self, _cx: &mut Cx, _request_id: LiveId, _progress: &HttpProgress) {}
    fn handle_http_stream(&mut self, _cx: &mut Cx, _request_id: LiveId, _data: &HttpResponse) {}
    fn handle_http_stream_complete(&mut self, _cx: &mut Cx, _request_id: LiveId, _data: &HttpResponse) {}

    // Signals
    fn handle_signal(&mut self, _cx: &mut Cx) {}

    // Media devices
    fn handle_audio_devices(&mut self, _cx: &mut Cx, _e: &AudioDevicesEvent) {}
    fn handle_midi_ports(&mut self, _cx: &mut Cx, _e: &MidiPortsEvent) {}
    fn handle_video_inputs(&mut self, _cx: &mut Cx, _e: &VideoInputsEvent) {}
}
標準的な App の定型コード(必須)

Makepad 2.0 のすべてのアプリには、次の Rust 構造が必要です。

use makepad_widgets::*;

app_main!(App);

script_mod! {
    // ... Splash UI code ...
}

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) {
        // Handle widget actions here
    }
}

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

重要:handle_event では、self.match_event(cx, event)(MatchEvent のハンドラーにイベントをディスパッチするため)と self.ui.handle_event(cx, event, &mut Scope::empty())(ウィジェットにイベントを伝播するため)の両方を呼び出す必要があります。


3. ウィジェットアクション API

Rust から self.ui.widget_type(cx, ids!(name)) を使ってウィジェットにアクセスし、&Actions 参照を渡してアクションの状態を確認します。

Button
// Access: self.ui.button(cx, ids!(my_button))
// Returns ButtonRef

.clicked(actions) -> bool           // Was clicked (finger down + up inside)
.pressed(actions) -> bool           // Was pressed down
.long_pressed(actions) -> bool      // Was long-pressed (not yet released)
.released(actions) -> bool          // Was released (NOT a click)
.clicked_modifiers(actions) -> Option<KeyModifiers>   // Clicked with modifier keys
.pressed_modifiers(actions) -> Option<KeyModifiers>
.released_modifiers(actions) -> Option<KeyModifiers>
TextInput
// Access: self.ui.text_input(cx, ids!(my_input))
// Returns TextInputRef

.changed(actions) -> Option<String>                    // Text changed, returns new text
.returned(actions) -> Option<(String, KeyModifiers)>   // Enter pressed, returns text + mods
.escaped(actions) -> bool                              // Escape pressed
.key_down_unhandled(actions) -> Option<KeyEvent>       // Unhandled key event
.selected_text() -> String                             // Current selection (no actions needed)
CheckBox
// Access: self.ui.check_box(cx, ids!(my_check))
// Returns CheckBoxRef

.changed(actions) -> Option<bool>   // Toggled, returns new checked state
DropDown
// Access: self.ui.drop_down(cx, ids!(my_dropdown))
// Returns DropDownRef

.selected(actions) -> Option<usize>        // Item selected, returns index
.changed(actions) -> Option<usize>         // Same as selected
.changed_label(actions) -> Option<String>  // Item selected, returns label string
.selected_item() -> usize                  // Current selection (no actions needed)
.selected_label() -> String                // Current label (no actions needed)
Slider
// Access: self.ui.slider(cx, ids!(my_slider))
// Returns SliderRef

.slided(actions) -> Option<f64>     // Value changed during slide or text edit
.end_slide(actions) -> Option<f64>  // Slide ended or text committed
.value() -> Option<f64>             // Current value (no actions needed)
RadioButton / RadioButtonGroup
// Access: self.ui.radio_button(cx, ids!(my_radio))
// Returns RadioButtonRef

.clicked(actions) -> bool                          // Was clicked
// For groups:
.selected(cx, actions) -> Option<usize>            // Selected index in group
LinkLabel
// Access: self.ui.link_label(cx, ids!(my_link))
// Returns LinkLabelRef

.clicked(actions) -> bool
.clicked_modifiers(actions) -> Option<KeyModifiers>
完全な例
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()
            });
        }

        if let Some(text) = self.ui.text_input(cx, ids!(search_input)).changed(actions) {
            self.perform_search(cx, &text);
        }

        if let Some(checked) = self.ui.check_box(cx, ids!(dark_mode)).changed(actions) {
            self.set_theme(cx, checked);
        }

        if let Some(index) = self.ui.drop_down(cx, ids!(language)).selected(actions) {
            self.change_language(cx, index);
        }
    }
}

4. script_eval! マクロ -- Rust から Splash への通信

script_eval! は Rust のハンドラー内から Splash コードを実行します。Rust から Splash の状態を更新し、UI の再レンダリングをトリガーするための主要なブリッジです。

シグネチャ:script_eval!(cx_or_vm, { splash_code })

第 1 引数には、イベントハンドラー内では &mut Cx、初期化時には &mut ScriptVm を指定できます。

// Update Splash state and re-render
if self.ui.button(cx, ids!(increment_button)).clicked(actions) {
    script_eval!(cx, {
        mod.state.counter += 1
        ui.main_view.render()
    });
}

// Pass Rust values into Splash using #(expr) interpolation
let rust_string = "Hello from Rust";
script_eval!(cx, {
    mod.value = #(rust_string)
});

// During init (inside App::run), use vm instead of cx
impl App {
    fn run(vm: &mut ScriptVm) -> Self {
        crate::makepad_widgets::theme_mod(vm);
        script_eval!(vm, {
            mod.theme = mod.themes.light
        });
        crate::makepad_widgets::widgets_mod(vm);
        App::from_script_mod(vm, self::script_mod)
    }
}
#(expr) による値の埋め込み

script_eval! 内で #(rust_expression) を使うと、Rust の値を Splash に埋め込めます。

let count = 42_u64;
let message = "items found".to_string();
script_eval!(cx, {
    mod.state.count = #(count)
    mod.state.message = #(message)
    ui.results_view.render()
});

5. script_apply_eval! マクロ -- 実行時のプロパティパッチ適用

script_apply_eval! は Rust から実行時にウィジェットのプロパティにパッチを適用します。一般的な Splash コードを実行する script_eval! とは異なり、特定のウィジェット参照を対象に、プロパティの変更を直接適用します。

シグネチャ:script_apply_eval!(cx, widget_ref, { property_patches })

// Patch a single property
let height = 500.0_f64;
script_apply_eval!(cx, item, {
    page_view: { height: #(height) }
});

// Patch margin
let margin = Inset { top: 10.0, bottom: 10.0, left: 5.0, right: 5.0 };
script_apply_eval!(cx, content_view, {
    margin: #(margin)
});

// Patch width with a Makepad type
script_apply_eval!(cx, content_view, {
    width: #(Size::fit())
});

// Patch nested draw shader properties
let bg_color = vec4(0.2, 0.3, 0.4, 1.0);
let triangle_height = 8.0_f64;
script_apply_eval!(cx, content, {
    draw_bg +: {
        triangle_height: #(triangle_height)
        background_color: #(bg_color)
    }
});

6. Rust からのウィジェット参照へのアクセス

ID によるウィジェットへのアクセス

self.ui.widget_type(cx, ids!(name)) を使って、型付きの参照を取得します。

self.ui.button(cx, ids!(increment_button))       // -> ButtonRef
self.ui.label(cx, ids!(status_label))             // -> LabelRef
self.ui.text_input(cx, ids!(search_input))        // -> TextInputRef
self.ui.check_box(cx, ids!(dark_mode))            // -> CheckBoxRef
self.ui.drop_down(cx, ids!(language))             // -> DropDownRef
self.ui.slider(cx, ids!(volume))                  // -> SliderRef
self.ui.view(cx, ids!(content))                   // -> ViewRef
self.ui.radio_button(cx, ids!(option_a))          // -> RadioButtonRef
ids! によるネストされたウィジェットへのアクセス

ドット記法を使って、ネストされたウィジェットにアクセスします。

self.ui.label(cx, ids!(container.inner.child_label))
self.ui.button(cx, ids!(toolbar.save_button))
汎用ウィジェットへのアクセス
self.ui.widget(cx, ids!(my_widget))    // -> WidgetRef (untyped)
よく使われるウィジェットのセッターメソッド(Rust から呼び出す場合)
// Label
self.ui.label(cx, ids!(my_label)).set_text(cx, "Hello");

// TextInput
self.ui.text_input(cx, ids!(my_input)).set_text(cx, "default value");

// CheckBox
if let Some(mut inner) = self.ui.check_box(cx, ids!(my_check)).borrow_mut() {
    inner.set_active(cx, true);
}

// DropDown
self.ui.drop_down(cx, ids!(my_dd)).set_selected_item(cx, 2);
self.ui.drop_down(cx, ids!(my_dd)).set_labels(cx, vec!["A".into(), "B".into()]);

// Slider
self.ui.slider(cx, ids!(my_slider)).set_value(cx, 0.5);

// Redraw any widget
self.ui.widget(cx, ids!(my_widget)).redraw(cx);

// Visibility
self.ui.button(cx, ids!(my_button)).set_visible(cx, false);

7. Splash からのウィジェットメソッド呼び出し

script_mod! 内の Splash コードでは、ui プレフィックスを使ってウィジェットにアクセスします。

// Trigger a re-render of a view's on_render handler
ui.main_view.render()

// Get text from a TextInput
let text = ui.todo_input.text()

// Set text on a TextInput
ui.todo_input.set_text("")

// Trigger a click on another widget
ui.add_button.on_click()

8. ヒットイベント(低レベル)

生の入力を処理する必要があるカスタムウィジェットでは、Hit enum が低レベルイベントを提供します。ウィジェットの handle_event 内で event.hits(cx, area) を使ってアクセスします。

pub enum Hit {
    // Keyboard focus
    KeyFocus(KeyFocusEvent),
    KeyFocusLost(KeyFocusEvent),

    // Keyboard input
    KeyDown(KeyEvent),
    KeyUp(KeyEvent),

    // Text input
    TextInput(TextInputEvent),
    TextRangeReplace(TextRangeReplaceEvent),
    TextCopy(TextClipboardEvent),
    TextCut(TextClipboardEvent),
    ImeAction(ImeActionEvent),

    // Pointer/finger events
    FingerDown(FingerDownEvent),
    FingerMove(FingerMoveEvent),
    FingerUp(FingerUpEvent),
    FingerScroll(FingerScrollEvent),
    FingerLongPress(FingerLongPressEvent),

    // Hover events
    FingerHoverIn(FingerHoverEvent),
    FingerHoverOver(FingerHoverEvent),
    FingerHoverOut(FingerHoverEvent),

    // Triggers
    Trigger(TriggerHitEvent),

    Nothing,
}
カスタムウィジェットでの使用方法
impl Widget for MyCustomWidget {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
        match event.hits(cx, self.draw_bg.area()) {
            Hit::FingerDown(fd) => {
                cx.set_key_focus(self.draw_bg.area());
                // fd.abs -- absolute position
                // fd.rel -- relative position within widget
                // fd.modifiers -- KeyModifiers
            }
            Hit::FingerUp(fu) => {
                if fu.is_over {
                    // Finger was released inside the widget = click
                    let uid = self.widget_uid();
                    cx.widget_action(uid, MyWidgetAction::Clicked);
                }
            }
            Hit::FingerMove(fm) => {
                // fm.abs, fm.rel -- current position
            }
            Hit::FingerHoverIn(_) => {
                self.animator_play(cx, ids!(hover.on));
            }
            Hit::FingerHoverOut(_) => {
                self.animator_play(cx, ids!(hover.off));
            }
            Hit::KeyDown(ke) => {
                match ke.key_code {
                    KeyCode::ReturnKey => { /* handle enter */ }
                    KeyCode::Escape => { /* handle escape */ }
                    _ => {}
                }
            }
            _ => {}
        }
    }
}

9. タイマーイベント

タイマーは Cx のメソッドで作成し、handle_timer で受け取ります。

タイマーの作成
// One-shot timer (fires once after delay in seconds)
let timer: Timer = cx.start_timeout(2.0);  // 2 seconds

// Repeating timer (fires every interval seconds)
let timer: Timer = cx.start_interval(0.5); // every 500ms

// Stop a timer
cx.stop_timer(timer);
タイマーイベントの処理
#[derive(Script, ScriptHook)]
pub struct App {
    #[live]
    ui: WidgetRef,
    #[rust]
    poll_timer: Timer,
}

impl MatchEvent for App {
    fn handle_startup(&mut self, cx: &mut Cx) {
        self.poll_timer = cx.start_interval(30.0);
    }

    fn handle_timer(&mut self, cx: &mut Cx, event: &TimerEvent) {
        if self.poll_timer.is_timer(event).is_some() {
            self.do_periodic_work(cx);
        }
    }
}

10. HTTP/ネットワークイベント

Rust からの HTTP リクエスト
let request_id = live_id!(my_request);
let mut req = HttpRequest::new(url, HttpMethod::Get);
req.set_header("Content-Type", "application/json");
req.set_body(body_bytes);
cx.http_request(request_id, req);
HTTP レスポンスの処理
impl MatchEvent for App {
    fn handle_http_response(&mut self, cx: &mut Cx, request_id: LiveId, response: &HttpResponse) {
        if request_id == live_id!(my_request) {
            let body = &response.body;
            // Process response...
        }
    }

    fn handle_http_request_error(&mut self, cx: &mut Cx, request_id: LiveId, err: &HttpError) {
        if request_id == live_id!(my_request) {
            log!("Request failed: {}", err.message);
        }
    }

    fn handle_http_progress(&mut self, cx: &mut Cx, request_id: LiveId, progress: &HttpProgress) {
        if request_id == live_id!(my_request) {
            let pct = progress.loaded as f64 / progress.total as f64;
            // Update progress UI...
        }
    }

    // For streaming responses
    fn handle_http_stream(&mut self, cx: &mut Cx, request_id: LiveId, data: &HttpResponse) {
        // Receive incremental chunks
    }

    fn handle_http_stream_complete(&mut self, cx: &mut Cx, request_id: LiveId, data: &HttpResponse) {
        // Stream finished
    }
}
Splash からの HTTP リクエスト
let req = net.HttpRequest{
    url: "https://api.example.com/data"
    method: net.HttpMethod.GET
    headers: {"Content-Type": "application/json"}
}
net.http_request(req) do net.HttpEvents{
    on_response: |res| {
        // handle response
        let data = res.body.to_string().parse_json()
    }
    on_error: |e| {
        // handle error
    }
}

// Streaming HTTP request
let req = net.HttpRequest{
    url: "https://api.openai.com/v1/chat/completions"
    method: net.HttpMethod.POST
    headers: {"Content-Type": "application/json"}
    is_streaming: true
    body: {model: "gpt-4" messages: [{role: "user" content: "Hello"}]}
}
net.http_request(req) do net.HttpEvents{
    on_stream: fn(res){
        // Process each streaming chunk
        let chunk = res.body.to_string()
    }
    on_complete: fn(res){
        // Stream finished
    }
}

11. イベントフロー図

User Input (click/key/mouse)
    |
    v
Platform Event Loop (Cx)
    |
    v
AppMain::handle_event(&mut self, cx, event)
    |
    +--> self.match_event(cx, event)
    |       |
    |       +--> MatchEvent::handle_startup / handle_shutdown / ...
    |       +--> MatchEvent::handle_actions(cx, actions)
    |       |       |
    |       |       +--> self.ui.button(cx, ids!(name)).clicked(actions)
    |       |       +--> script_eval!(cx, { ... })
    |       |               |
    |       |               v
    |       |           ScriptVm::eval -> updates Splash state
    |       |               |
    |       |               +--> ui.view.render() -> schedules re-draw
    |       |
    |       +--> MatchEvent::handle_timer(cx, event)
    |       +--> MatchEvent::handle_http_response(cx, id, response)
    |       +--> MatchEvent::handle_key_down(cx, event)
    |
    +--> self.ui.handle_event(cx, event, scope)
            |
            +--> Widget tree event propagation
            +--> Hit detection (FingerDown/Up/Move)
            +--> Splash on_click / on_return handlers execute
            +--> on_render handlers execute during draw
            +--> Widget actions emitted -> feed back into Actions

12. 完全なパターン

パターン 1:カウンター(ボタンクリック -> 状態更新 -> 再レンダリング)
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, 220)
                body +: {
                    main_view := View{
                        width: Fill
                        height: Fill
                        flow: Down
                        spacing: 12
                        align: Center
                        on_render: ||{
                            counter_label := 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());
    }
}
パターン 2:フォーム(テキスト入力 -> 検証 -> 送信)
// Inside script_mod!

fn submit_form(){
    let name = ui.name_input.text()
    let email = ui.email_input.text()
    if name == "" {
        ui.error_label.set_text("Name is required")
    } else if email == "" {
        ui.error_label.set_text("Email is required")
    } else {
        // Process form...
        ui.error_label.set_text("")
        ui.name_input.set_text("")
        ui.email_input.set_text("")
    }
}

// In the UI tree:
name_input := TextInput{
    empty_text: "Your name"
}
email_input := TextInput{
    empty_text: "[email protected]"
    on_return: || submit_form()
}
submit_button := Button{
    text: "Submit"
    on_click: || submit_form()
}
error_label := Label{
    text: ""
    draw_text.color: #f00
}
パターン 3:項目ごとのイベントを持つリスト
let items = []
items.push({name: "Item A", selected: false})
items.push({name: "Item B", selected: false})

fn toggle_item(index, checked){
    items[index].selected = checked
}

fn delete_item(index){
    items.remove(index)
    ui.item_list.render()
}

// In UI:
item_list := ScrollYView{
    width: Fill height: Fill
    new_batch: true
    on_render: ||{
        for i, item in items {
            RoundedView{
                width: Fill height: Fit
                flow: Right
                check := CheckBox{
                    active: item.selected
                    on_click: |checked| toggle_item(i, checked)
                }
                Label{text: item.name}
                Button{
                    text: "Delete"
                    on_click: || delete_item(i)
                }
            }
        }
    }
}
パターン 4:レイヤー間の通信(Splash の状態 + Rust のロジック)
// Rust side: handle complex logic, then update Splash state
impl MatchEvent for App {
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        if self.ui.button(cx, ids!(fetch_button)).clicked(actions) {
            // Rust handles the HTTP request
            let request_id = live_id!(fetch_data);
            let req = HttpRequest::new("https://api.example.com/data", HttpMethod::Get);
            cx.http_request(request_id, req);
        }
    }

    fn handle_http_response(&mut self, cx: &mut Cx, request_id: LiveId, response: &HttpResponse) {
        if request_id == live_id!(fetch_data) {
            let data = String::from_utf8_lossy(&response.body).to_string();
            // Bridge back to Splash to update UI
            script_eval!(cx, {
                mod.state.data = #(data)
                mod.state.loading = false
                ui.results_view.render()
            });
        }
    }
}

ソースファイルの参照先

ファイル用途
draw/src/match_event.rsMatchEvent トレイトの定義
widgets/src/widget_match_event.rsWidgetMatchEvent トレイト(カスタムウィジェット用)
platform/src/event/event.rsEvent enum、Hit enum、Timer
platform/src/event/finger.rsフィンガー/ポインターイベントの型
platform/script/src/lib.rsscript_eval! マクロの定義
platform/script/derive/src/lib.rsscript_apply_eval! プロシージャルマクロ
widgets/src/button.rsボタンのアクション API
widgets/src/text_input.rsTextInput のアクション API
widgets/src/check_box.rsCheckBox のアクション API
widgets/src/drop_down.rsDropDown のアクション API
widgets/src/slider.rsSlider のアクション API
widgets/src/radio_button.rsRadioButton のアクション API
examples/counter/src/app.rsカウンターの例(最小構成)
examples/todo/src/app.rsTodo の例(包括的)
examples/git/src/app.rsGit の例(HTTP、タイマー)
examples/camera/src/カメラの例(メディアイベント)
examples/text_selection/src/テキスト選択の例

13. 新しいイベント(2026 年 3 月)

選択とクリップボードのイベント

テキスト選択とクリップボード操作用の新しい Hit バリアント:

Hit::TextCopy(TextClipboardEvent)  // User copied text (Ctrl+C or native toolbar)
Hit::TextCut(TextClipboardEvent)   // User cut text (Ctrl+X or native toolbar)

モバイルの選択ハンドル: iOS/Android のネイティブ選択ハンドルは、次のイベントを発行します:

Event::SelectionHandleDrag  // User dragging a selection handle

テキストを長押しすると単語が選択され、ネイティブのクリップボードツールバーが表示されます。TextFlow ウィジェットはクリップボード操作を自動的に統合します。

ポップアップウィンドウのイベント

ポップアップウィンドウ(コンテキストメニュー、ドロップダウン)用:

Event::WindowClosed    // Popup window was closed
Event::PopupDismissed  // Popup was dismissed by compositor (Wayland) or click-outside

WindowClosed は常に PopupDismissed より先に発行されます。アプリで明示的なクローズのセマンティクスを実現するには、PopupDismissed を処理する必要があります。

IME イベント

CJK テキスト入力向けの IME(入力メソッドエディター)サポート:

Hit::ImeAction(ImeActionEvent)     // IME composition committed/cancelled
Hit::TextRangeReplace(TextRangeReplaceEvent)  // IME text replacement

Linux X11 IME: X11 の完全な IME サポートが追加されました。IME ポップアップウィンドウが TextInput のフォーカスなしで表示されないよう、ウィンドウの位置が修正されました。

ビデオとメディアのイベント
// In MatchEvent trait:
fn handle_video_inputs(&mut self, _cx: &mut Cx, _e: &VideoInputsEvent) {}

メディアプラグインシステムからのビデオ入力デバイス列挙イベントです。カメラとビデオ再生のイベントは、メディアプラグインアーキテクチャを通じてルーティングされます。

主選択(Linux)

Linux の主選択(ミドルクリックで貼り付け):

  • Wayland: zwp_primary_selection_device_manager_v1 プロトコル
  • X11: PRIMARY アトムの処理
  • TextInput および TextFlow ウィジェットと自動的に統合されます

コメント