gpui-patterns について
GPUIで保守しやすいアプリを開発するためのリファレンススキルです。コンポーネント構成、状態管理、イベント、アクション、描画、ライフサイクルのパターンをまとめています。
主な機能
- 01コンポーネント構成パターン
- 02モデル・ビューとコンテキスト状態
- 03サブスクリプションと選択的更新
- 04マウス・キーボード・クリック処理
- 05アクションとキーバインド
- 06描画とライフサイクルの指針
活用シーン
- 01GPUIのビューコンポーネント構成
- 02状態管理と購読パターンの選択
- 03入力処理とアクションの実装
- 04描画とライフサイクルのコード確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ geoffjay/ claude-plugins --skill gpui-patternsインストール後、Claude Code で/gpui-patternsを入力して読み込まれたか確認します。
詳細
GPUI パターン
メタデータ
このスキルでは、保守性とパフォーマンスに優れたアプリケーションを構築するための、一般的な GPUI パターンとベストプラクティスを包括的に説明します。
手順
コンポーネントの合成パターン
基本的なコンポーネント構造
use gpui::*;
// View component with state
struct MyView {
state: Model<MyState>,
_subscription: Subscription,
}
impl MyView {
fn new(state: Model<MyState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&state, |_, _, cx| cx.notify());
Self { state, _subscription }
}
}
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.state.read(cx);
div()
.flex()
.flex_col()
.child(format!("Value: {}", state.value))
}
}
コンテナー/プレゼンターパターン
コンテナー(状態とロジックを管理):
struct Container {
model: Model<AppState>,
_subscription: Subscription,
}
impl Container {
fn new(model: Model<AppState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
impl Render for Container {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.model.read(cx);
// Pass data to presenter
Presenter::new(state.data.clone())
}
}
プレゼンター(純粋なレンダリング):
struct Presenter {
data: String,
}
impl Presenter {
fn new(data: String) -> Self {
Self { data }
}
}
impl Render for Presenter {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child(self.data.as_str())
}
}
複合コンポーネント
// Parent component with shared context
pub struct Tabs {
items: Vec<TabItem>,
active_index: usize,
}
pub struct TabItem {
label: String,
content: Box<dyn Fn() -> AnyElement>,
}
impl Tabs {
pub fn new() -> Self {
Self {
items: Vec::new(),
active_index: 0,
}
}
pub fn add_tab(
mut self,
label: impl Into<String>,
content: impl Fn() -> AnyElement + 'static,
) -> Self {
self.items.push(TabItem {
label: label.into(),
content: Box::new(content),
});
self
}
fn set_active(&mut self, index: usize, cx: &mut ViewContext<Self>) {
self.active_index = index;
cx.notify();
}
}
impl Render for Tabs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.child(
// Tab headers
div()
.flex()
.children(
self.items.iter().enumerate().map(|(i, item)| {
tab_header(&item.label, i == self.active_index, || {
self.set_active(i, cx)
})
})
)
)
.child(
// Active tab content
(self.items[self.active_index].content)()
)
}
}
状態管理の戦略
モデル・ビュー・パターン
// Model: Application state
#[derive(Clone)]
struct AppState {
count: usize,
items: Vec<String>,
}
// View: Observes and renders state
struct AppView {
state: Model<AppState>,
_subscription: Subscription,
}
impl AppView {
fn new(state: Model<AppState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&state, |_, _, cx| cx.notify());
Self { state, _subscription }
}
fn increment(&mut self, cx: &mut ViewContext<Self>) {
self.state.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}
}
コンテキストベースの状態管理
// Global state via context
#[derive(Clone)]
struct GlobalSettings {
theme: Theme,
language: String,
}
impl Global for GlobalSettings {}
// Initialize in app
fn init_app(cx: &mut AppContext) {
cx.set_global(GlobalSettings {
theme: Theme::Light,
language: "en".to_string(),
});
}
// Access in components
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = cx.global::<GlobalSettings>();
div()
.child(format!("Language: {}", settings.language))
}
}
サブスクリプションのパターン
基本的なサブスクリプション:
struct Observer {
model: Model<Data>,
_subscription: Subscription,
}
impl Observer {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| {
cx.notify(); // Rerender on change
});
Self { model, _subscription }
}
}
選択的な更新:
impl Observer {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |this, model, cx| {
let data = model.read(cx);
// Only rerender if specific field changed
if data.important_field != this.cached_field {
this.cached_field = data.important_field.clone();
cx.notify();
}
});
Self {
model,
cached_field: String::new(),
_subscription,
}
}
}
複数のサブスクリプション:
struct MultiObserver {
model_a: Model<DataA>,
model_b: Model<DataB>,
_subscriptions: Vec<Subscription>,
}
impl MultiObserver {
fn new(
model_a: Model<DataA>,
model_b: Model<DataB>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(&model_a, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&model_b, |_, _, cx| cx.notify()));
Self {
model_a,
model_b,
_subscriptions: subscriptions,
}
}
}
イベント処理のパターン
クリックイベント
div()
.on_click(cx.listener(|this, event: &ClickEvent, cx| {
// Handle click
this.handle_click(cx);
}))
.child("Click me")
キーボードイベント
div()
.on_key_down(cx.listener(|this, event: &KeyDownEvent, cx| {
match event.key.as_str() {
"Enter" => this.submit(cx),
"Escape" => this.cancel(cx),
_ => {}
}
}))
イベントの伝播
// Stop propagation
div()
.on_click(|event, cx| {
event.stop_propagation();
// Handle click
})
// Prevent default
div()
.on_key_down(|event, cx| {
if event.key == "Tab" {
event.prevent_default();
// Custom tab handling
}
})
マウスイベント
div()
.on_mouse_down(cx.listener(|this, event, cx| {
this.mouse_down_position = Some(event.position);
}))
.on_mouse_move(cx.listener(|this, event, cx| {
if let Some(start) = this.mouse_down_position {
let delta = event.position - start;
this.handle_drag(delta, cx);
}
}))
.on_mouse_up(cx.listener(|this, event, cx| {
this.mouse_down_position = None;
}))
アクションシステム
アクションの定義
use gpui::*;
actions!(app, [
Increment,
Decrement,
Reset,
SetValue
]);
// Action with data
#[derive(Clone, PartialEq)]
pub struct SetValue {
pub value: i32,
}
impl_actions!(app, [SetValue]);
アクションハンドラーの登録
impl Counter {
fn register_actions(&mut self, cx: &mut ViewContext<Self>) {
cx.on_action(cx.listener(|this, _: &Increment, cx| {
this.model.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}));
cx.on_action(cx.listener(|this, _: &Decrement, cx| {
this.model.update(cx, |state, cx| {
state.count = state.count.saturating_sub(1);
cx.notify();
});
}));
cx.on_action(cx.listener(|this, action: &SetValue, cx| {
this.model.update(cx, |state, cx| {
state.count = action.value;
cx.notify();
});
}));
}
}
アクションのディスパッチ
// From within component
fn handle_button_click(&mut self, cx: &mut ViewContext<Self>) {
cx.dispatch_action(Increment);
}
// With data
fn set_specific_value(&mut self, value: i32, cx: &mut ViewContext<Self>) {
cx.dispatch_action(SetValue { value });
}
// Global action dispatch
cx.dispatch_action_on_window(Reset, window_id);
キーバインド
// Register global keybindings
fn register_keybindings(cx: &mut AppContext) {
cx.bind_keys([
KeyBinding::new("cmd-+", Increment, None),
KeyBinding::new("cmd--", Decrement, None),
KeyBinding::new("cmd-0", Reset, None),
]);
}
エレメントの合成
ビルダーパターン
fn card(title: &str, content: impl IntoElement) -> impl IntoElement {
div()
.flex()
.flex_col()
.bg(white())
.border_1()
.rounded_lg()
.shadow_sm()
.p_6()
.child(
div()
.text_lg()
.font_semibold()
.mb_4()
.child(title)
)
.child(content)
}
条件付きレンダリング
div()
.when(condition, |this| {
this.bg(blue_500())
})
.when_some(optional_value, |this, value| {
this.child(format!("Value: {}", value))
})
.map(|this| {
if complex_condition {
this.border_1()
} else {
this.border_2()
}
})
動的な子要素
div()
.children(
items.iter().map(|item| {
div().child(item.name.as_str())
})
)
ビューのライフサイクル
初期化
impl MyView {
fn new(cx: &mut ViewContext<Self>) -> Self {
// Initialize state
let model = cx.new_model(|_| MyState::default());
// Set up subscriptions
let subscription = cx.observe(&model, |_, _, cx| cx.notify());
// Spawn async tasks
cx.spawn(|this, mut cx| async move {
// Async initialization
}).detach();
Self {
model,
_subscription: subscription,
}
}
}
更新通知
impl MyView {
fn update_state(&mut self, new_data: Data, cx: &mut ViewContext<Self>) {
self.model.update(cx, |state, cx| {
state.data = new_data;
cx.notify(); // Trigger rerender
});
}
}
クリーンアップ
impl Drop for MyView {
fn drop(&mut self) {
// Manual cleanup if needed
// Subscriptions are automatically dropped
}
}
リアクティブパターン
派生状態
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.model.read(cx);
// Compute derived values
let total = state.items.iter().map(|i| i.value).sum::<i32>();
let average = total / state.items.len() as i32;
div()
.child(format!("Total: {}", total))
.child(format!("Average: {}", average))
}
}
非同期更新
impl MyView {
fn load_data(&mut self, cx: &mut ViewContext<Self>) {
let model = self.model.clone();
cx.spawn(|_, mut cx| async move {
let data = fetch_data().await?;
cx.update_model(&model, |state, cx| {
state.data = data;
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
リソース
公式ドキュメント
- GPUI GitHub: https://github.com/zed-industries/zed/tree/main/crates/gpui
- Zed Editor のソース:実際の GPUI の使用例
一般的なパターンのリファレンス
- モデル・ビュー:状態管理パターン
- コンテナー・プレゼンター:関心事の分離
- 複合コンポーネント:連携する関連コンポーネント
- アクションシステム:ユーザー操作のためのコマンドパターン
- サブスクリプション:リアクティブな更新のためのオブザーバーパターン
ベストプラクティス
- クリーンアップを防ぐため、サブスクリプションを保持します
cx.notify()は控えめに使用します- 継承よりも合成を優先します
- レンダリングメソッドは純粋に保ちます
- エラーを適切に処理します
- コンポーネント API を文書化します
- コンポーネントの動作をテストします
コメント
Markdown 対応