6. 基本ウィジェットとフォーム制御
ボタン(Button)とバリエーション
画面操作の基本となる Button コンポーネントは、gpui_component::button モジュールに含まれています。
Button は内部状態を持たないステートレス部品(RenderOnce)なので、render メソッドの中で毎回 Button::new("一意なID") を呼び出して組み立てます。
▸ バリエーション(見た目)の切り替え
ButtonVariants トレイトをインポートすると、用途に応じた 6 種類のスタイルをメソッド 1 つで切り替えられます。
.primary(): 画面内で最も重要なメイン操作(保存、送信、追加など)。.default()(既定): 標準的なセカンダリ操作(キャンセル、閉じるなど)。.danger(): 取り消せない危険な操作(削除、リセットなど。赤色で強調されます)。.warning(): 注意を促す操作。.ghost(): 背景や枠線がなく、マウスを乗せたときだけ背景が出る控えめなボタン(ツールバーなどに最適)。.link(): Web のハイパーリンクのように見えるテキストボタン。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::{
button::{Button, ButtonVariants as _},
h_flex, IconName, Sizable as _, Disableable as _,
};
fn render_buttons() -> impl IntoElement {
h_flex()
.gap_3()
// 1. プライマリボタン
.child(
Button::new("save-btn")
.primary()
.label("保存する")
.on_click(|_event, _window, _cx| {
println!("保存が実行されました");
}),
)
// 2. アイコン付きの小サイズ(Small)削除ボタン
.child(
Button::new("delete-btn")
.danger()
.small()
.icon(IconName::Delete)
.label("削除")
.on_click(|_event, _window, _cx| {
println!("削除が実行されました");
}),
)
// 3. ローディング中・操作無効化ボタン
.child(
Button::new("loading-btn")
.label("通信中...")
.loading(true) // スピナーアイコンが回り、クリックが防止される
.disabled(true), // 明示的に無効化する場合
)
}
}
アイコンだけの正方形ボタンを作りたいときも、Button::new("icon-btn").ghost().icon(IconName::Settings) のように .label() を省略するだけで自動的に美しいアイコンボタンになります。
アイコン・バッジ・アバター・タグ
文字だけでなく視覚的なシンボルを添えることで、画面の分かりやすさは劇的に向上します。 GPUI-Component には、すぐ使える表示用ウィジェットが揃っています。
▸ 1. アイコン(Icon と IconName)
GPUI-Component には、検索、設定、チェックマーク、矢印、フォルダ、ゴミ箱など、アプリ開発で頻繁に使う SVG アイコンが IconName 列挙型としてあらかじめ同梱されています。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _};
fn render_icons(cx: &App) -> impl IntoElement {
Icon::new(IconName::Search)
.small()
.text_color(cx.theme().muted_foreground)
}
}
もちろん、独自の SVG ファイルを使いたい場合も Icon::empty().path("icons/custom.svg") のようにアセットパスを指定して表示できます。
▸ 2. タグ(Tag)とバッジ(Badge)
Tag: タスクのカテゴリやステータス(「進行中」「重要」など)を示す色付きの小さなラベルです。Badge: アイコンやアバターの右上に未読件数(数字)や赤いドットを重ねて表示する部品です。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::{badge::Badge, h_flex, tag::Tag, Icon, IconName};
fn render_status_indicators() -> impl IntoElement {
h_flex()
.gap_4()
// 色付きタグ
.child(Tag::primary().child("新機能"))
.child(Tag::danger().child("優先度:高"))
// アイコンの右上に未読数「5」のバッジを重ねる
.child(
Badge::new()
.count(5)
.child(Icon::new(IconName::Bell)),
)
}
}
テキスト入力(InputとInputState)
GUI フレームワークを自作するときに最も開発者を悩ませるのが テキスト入力欄 です。
キーボードの文字入力だけでなく、日本語や中国語の変換(IME 候補ウィンドウの位置合わせ)、マウスドラッグによる範囲選択、クリップボードへのコピー&ペースト(Ctrl+C / Ctrl+V)、取り消し(Ctrl+Z)、左右矢印キーでのカーソル移動など、膨大な処理が必要になるからです。
GPUI-Component の Input(および InputState)は、これらすべての機能を最初から完璧に備えています。
▸ InputState(状態)と Input(見た目)のペアで使う
テキスト入力欄は内部にカーソル位置や選択範囲などの複雑な状態を持つため、以下の 2 段階で扱います。
- 親ビューの初期化時(
new):cx.new(|cx| InputState::new(window, cx))を呼んで、状態エンティティ(Entity<InputState>)を作成し、親ビューのフィールドに保存します。 - 親ビューの描画時(
render):Input::new(&self.input_state)を呼んで画面に配置します。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::{
button::{Button, ButtonVariants as _},
h_flex,
input::{Input, InputEvent, InputState},
v_flex,
};
pub struct FormDemo {
name_input: Entity<InputState>,
submitted_text: String,
}
impl FormDemo {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
// 1. InputState エンティティを作成する
let name_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("お名前を入力してください...")
.clean_on_escape() // Escキーで入力をクリアするオプション
});
// 2. 入力内容の変化や Enter キー押下を購読(subscribe)する
cx.subscribe_in(&name_input, window, |this, state, event: &InputEvent, window, cx| {
match event {
InputEvent::Change => {
// 文字が入力・削除されるたびに呼ばれる
let current = state.read(cx).value();
println!("入力中の文字列: {}", current);
}
InputEvent::PressEnter { .. } => {
// Enter キーが押されたら送信処理を実行する
this.submit(window, cx);
}
_ => {}
}
})
.detach();
Self {
name_input,
submitted_text: String::new(),
}
}
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// InputState から現在の文字列を取り出す
let text = self.name_input.read(cx).value().to_string();
if !text.is_empty() {
self.submitted_text = format!("こんにちは、{} さん!", text);
// 入力欄を空に戻す
self.name_input.update(cx, |state, cx| {
state.set_value("", window, cx);
});
cx.notify();
}
}
}
impl Render for FormDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_3()
.p_4()
.child(
h_flex()
.gap_2()
// 3. Input::new に &Entity<InputState> を渡して描画する
.child(
Input::new(&self.name_input)
.cleanable(true), // 右端にクリア用の「×」ボタンを表示
)
.child(
Button::new("submit-btn")
.primary()
.label("送信")
.on_click(cx.listener(|this, _event, window, cx| {
this.submit(window, cx);
})),
),
)
.child(self.submitted_text.clone())
}
}
}
さらに、InputState にはパスワード入力用の .masked(true) や、複数行のテキストエリアにする .multi_line()、検索アイコンを左端に付ける Input::new(&state).prefix(Icon::new(IconName::Search)) などの便利なメソッドも用意されています。
選択系ウィジェット(Checkbox・Switch・Radio・Select)
設定画面やフィルタ画面でよく使う選択系のウィジェットも、直感的に記述できます。
▸ 1. Checkbox(チェックボックス)と Switch(トグルスイッチ)
どちらも真偽値(bool)を渡して表示し、クリックされたときに新しい bool 値を受け取るステートレス部品です。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::{checkbox::Checkbox, h_flex, switch::Switch};
// ビューのフィールドに `agreed: bool` と `notifications_enabled: bool` がある場合の描画例
fn render_toggles(
agreed: bool,
notifications_enabled: bool,
cx: &mut Context<MySettingsView>,
) -> impl IntoElement {
h_flex()
.gap_6()
// チェックボックス
.child(
Checkbox::new("agree-check")
.label("利用規約に同意する")
.checked(agreed)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.agreed = *checked;
cx.notify();
})),
)
// トグルスイッチ
.child(
Switch::new("notify-switch")
.label("デスクトップ通知を有効にする")
.checked(notifications_enabled)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.notifications_enabled = *checked;
cx.notify();
})),
)
}
}
▸ 2. Select(ドロップダウン選択メニュー)
複数の選択肢から 1 つを選ぶドロップダウンメニューは、テキスト入力と同様に状態を持つため SelectState エンティティと Select ウィジェットを組み合わせて使います。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::select::{Select, SelectEvent, SelectState};
// 初期化時(cx.new の中): 選択肢のリストと初期選択インデックス(Some(0) など)を渡す
let role_select = cx.new(|cx| {
SelectState::new(
vec!["管理者 (Admin)", "編集者 (Editor)", "閲覧者 (Viewer)"],
Some(IndexPath::default()), // 最初の項目(0番目)を選択状態にする
window,
cx,
)
});
// 選択が変更されたときのイベントを購読する
cx.subscribe_in(&role_select, window, |this, _state, event: &SelectEvent<Vec<&'static str>>, _window, cx| {
if let SelectEvent::Confirm(selected_value) = event {
println!("選択されたロール: {:?}", selected_value);
cx.notify();
}
})
.detach();
// render メソッド内での配置:
// .child(Select::new(&self.role_select))
}
モーダル・ドロワー・通知(Notification)
第 5 章でウィンドウ全体を Root コンポーネントで包んだおかげで、確認ダイアログ(Dialog / Modal)やトースト通知(Notification)は、特別な状態フィールドを作らなくても window の拡張メソッド(WindowExt)を呼ぶだけ でいつでも画面上に表示できます。
▸ 1. トースト通知(window.push_notification)
保存が完了したときやエラーが起きたときに、画面の隅に数秒間だけメッセージを表示して自動で消える通知です。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::{button::Button, notification::Notification, WindowExt as _};
Button::new("notify-btn")
.label("保存テスト")
.on_click(|_event, window, cx| {
// 文字列を渡すだけのシンプルな通知、または Notification::success / error で種類を指定
window.push_notification(
Notification::success("ファイルが正常に保存されました。"),
cx,
);
})
}
▸ 2. 確認ダイアログ・モーダル(window.open_dialog)
重要な操作の前にユーザーの意思を確認したり、別フォームを入力させたりするモーダルウィンドウです。
#![allow(unused)]
fn main() {
use gpui::*;
use gpui_component::{button::Button, WindowExt as _};
Button::new("confirm-delete-btn")
.danger()
.label("データを初期化する")
.on_click(|_event, window, cx| {
// ウィンドウの最前面にモーダルダイアログを開く
window.open_dialog(cx, |dialog, _window, _cx| {
dialog
.title("本当に初期化しますか?")
.child("この操作は取り消すことができません。すべてのタスクデータが削除されます。")
.confirm() // 「OK」と「キャンセル」の標準ボタンを配置
.on_ok(|_event, window, cx| {
window.push_notification("データを初期化しました", cx);
true // true を返すとダイアログが閉じる
})
});
})
}
同様に、画面の左右や上下の端からスッと滑り出てくるスライドパネル(ドロワー)も、window.open_drawer(cx, |drawer, window, cx| { ... }) というまったく同じ書き方で呼び出すことができます。
これで、一般的な業務アプリやツールに必要な基本ウィジェットとフォーム制御が一通り揃いました。 次章では、GPUI と GPUI-Component の真骨頂である「数十万件のデータを一瞬で描画する仮想化リスト・データテーブル」と「画面を自在に分割する Dock システム」を解説します。