egui
Rustで書かれた即座に使えるGUIライブラリ。宣言的APIとリアクティブなUI更新を特徴とし、ゲーム、ツール、プロトタイピングに最適。WebAssembly対応により、ブラウザでも動作可能。
GitHub概要
emilk/egui
egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
トピックス
スター履歴
フレームワーク
egui
概要
egui(「いーぐい」)は、Rust言語で記述されたシンプル、高速、高ポータビリティなimmediate mode GUIライブラリです。Web、ネイティブ、お気に入りのゲームエンジンで動作し、ゲーム開発やツール作成において特に威力を発揮します。
詳細
egui(pronounced "e-gooey")は、immediate modeパラダイムを採用したRust製GUIフレームワークとして、2025年現在ゲーム開発とツール開発の分野で急速に注目を集めています。60FPSでのレスポンス性を重視した設計により、リアルタイム性が要求されるアプリケーションでの利用が拡大しています。
immediate modeアーキテクチャの特徴として、GUIは毎フレーム再描画され、コールバックやretained stateの管理が不要になり、コード構造が大幅に簡素化されます。これにより、状態を入力として受け取り、単一フレームを出力する純粋関数として扱えるため、デバッグUIやツール開発において直感的な実装が可能です。
eguiは100%純粋Rustで記述され、外部Cライブラリに依存しないため、メモリ安全性を保ちながら高いパフォーマンスを実現します。WebAssemblyサポートにより、ブラウザでの実行も可能で、真のクロスプラットフォーム対応を実現しています。
メリット・デメリット
メリット
- immediate mode設計: コールバック不要で直感的なプログラミング
- 高いレスポンス性: 60FPS対応でリアルタイムアプリに最適
- 完全Rust実装: メモリ安全性と外部依存ゼロ
- 優れたポータビリティ: Web、Linux、macOS、Windows、Android対応
- ゲームエンジン統合: Bevy等人気フレームワークとの統合
- 学習コストの低さ: Rustエコシステム最もアクセスしやすいGUI
- アンチエイリアス描画: 線、円、テキスト、凸多角形の高品質レンダリング
デメリット
- 複雑なレイアウト制約: immediate modeによる複雑なGUIレイアウトの困難
- ネイティブルック制限: プラットフォーム固有の見た目への対応が限定的
- 成熟度: 機能や安定性で retained mode ライブラリに劣る面
- API変更: 新しいリリースでbreaking changesの可能性
- IME対応不完全: 日本語など複雑な文字入力への制約
- 高度な機能不足: retained modeライブラリに比べ機能制限
主要リンク
書き方の例
Hello World アプリケーション
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(320.0, 240.0)),
..Default::default()
};
eframe::run_native(
"Hello egui",
options,
Box::new(|_cc| Box::new(MyApp::default())),
)
}
struct MyApp {
name: String,
age: u32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
name: "Arthur".to_owned(),
age: 42,
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application");
ui.horizontal(|ui| {
let name_label = ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name)
.labelled_by(name_label.id);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Click each year").clicked() {
self.age += 1;
}
ui.label(format!("Hello '{}', age {}", self.name, self.age));
});
}
}
カスタムウィジェットとペイント
use eframe::egui;
use egui::{Color32, Pos2, Rect, Stroke};
struct CustomWidget {
angle: f32,
}
impl Default for CustomWidget {
fn default() -> Self {
Self { angle: 0.0 }
}
}
impl eframe::App for CustomWidget {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Custom Drawing");
// アニメーション用の角度更新
self.angle += 0.02;
ctx.request_repaint();
let (rect, response) = ui.allocate_exact_size(
egui::Vec2::splat(200.0),
egui::Sense::drag()
);
if ui.is_rect_visible(rect) {
let painter = ui.painter();
let center = rect.center();
let radius = rect.width() / 4.0;
// 背景円を描画
painter.circle_filled(center, radius, Color32::BLUE);
// 回転する線を描画
let end_pos = center + radius * egui::Vec2::angled(self.angle);
painter.line_segment(
[center, end_pos],
Stroke::new(3.0, Color32::WHITE)
);
// マウスの位置を表示
if let Some(pointer_pos) = response.interact_pointer_pos() {
painter.circle_filled(pointer_pos, 5.0, Color32::RED);
}
}
ui.horizontal(|ui| {
ui.label("Animation angle:");
ui.add(egui::Slider::new(&mut self.angle, 0.0..=6.28));
});
});
}
}
複雑なレイアウトとウィンドウ管理
use eframe::egui;
struct MultiWindowApp {
show_settings: bool,
show_debug: bool,
counter: i32,
text: String,
}
impl Default for MultiWindowApp {
fn default() -> Self {
Self {
show_settings: false,
show_debug: false,
counter: 0,
text: String::new(),
}
}
}
impl eframe::App for MultiWindowApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// メニューバー
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Settings").clicked() {
self.show_settings = true;
}
if ui.button("Debug").clicked() {
self.show_debug = true;
}
ui.separator();
if ui.button("Quit").clicked() {
std::process::exit(0);
}
});
});
});
// サイドパネル
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Control Panel");
if ui.button("Increment").clicked() {
self.counter += 1;
}
if ui.button("Decrement").clicked() {
self.counter -= 1;
}
ui.separator();
ui.label("Text input:");
ui.text_edit_multiline(&mut self.text);
});
// メインパネル
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Main Area");
ui.label(format!("Counter: {}", self.counter));
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label("Scrollable content:");
for i in 0..100 {
ui.label(format!("Line {}", i));
}
});
});
// 設定ウィンドウ
egui::Window::new("Settings")
.open(&mut self.show_settings)
.show(ctx, |ui| {
ui.label("Settings panel");
ui.separator();
if ui.button("Reset counter").clicked() {
self.counter = 0;
}
if ui.button("Clear text").clicked() {
self.text.clear();
}
});
// デバッグウィンドウ
egui::Window::new("Debug")
.open(&mut self.show_debug)
.show(ctx, |ui| {
ui.label("Debug information");
ui.separator();
ui.label(format!("FPS: {:.1}", 1.0 / ctx.input(|i| i.stable_dt)));
ui.label(format!("Frame time: {:.2}ms", ctx.input(|i| i.stable_dt) * 1000.0));
ui.label(format!("Text length: {}", self.text.len()));
});
}
}
ゲームエンジン統合(Bevy example)
// Cargo.tomlに以下を追加:
// [dependencies]
// bevy = "0.12"
// bevy_egui = "0.23"
use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts, EguiPlugin};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(EguiPlugin)
.add_systems(Update, ui_system)
.run();
}
fn ui_system(mut contexts: EguiContexts) {
egui::Window::new("Game Debug").show(contexts.ctx_mut(), |ui| {
ui.label("Game is running!");
if ui.button("Spawn Entity").clicked() {
// ゲームエンティティの生成処理
println!("Entity spawned!");
}
ui.separator();
ui.label("Performance metrics:");
// ゲームの状態表示
ui.horizontal(|ui| {
ui.label("Entities:");
ui.label("42"); // 実際の値に置き換え
});
});
}
プロットとグラフ表示
use eframe::egui;
use egui_plot::{Line, Plot, PlotPoints};
struct PlotApp {
data: Vec<[f64; 2]>,
}
impl Default for PlotApp {
fn default() -> Self {
Self {
data: (0..100)
.map(|i| {
let x = i as f64 * 0.1;
[x, x.sin()]
})
.collect(),
}
}
}
impl eframe::App for PlotApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Data Visualization");
Plot::new("sine_wave")
.view_aspect(2.0)
.show(ui, |plot_ui| {
plot_ui.line(Line::new(PlotPoints::from(self.data.clone())));
});
ui.horizontal(|ui| {
if ui.button("Add Point").clicked() {
let x = self.data.len() as f64 * 0.1;
self.data.push([x, x.sin()]);
}
if ui.button("Clear").clicked() {
self.data.clear();
}
if ui.button("Reset").clicked() {
*self = Self::default();
}
});
});
}
}
Web向けビルドとプロジェクトセットアップ
# 新しいRustプロジェクト作成
cargo new egui-app
cd egui-app
# Cargo.tomlの依存関係設定
# [dependencies]
# eframe = "0.24"
# egui = "0.24"
# ネイティブ実行
cargo run
# Webビルドのセットアップ
cargo install trunk
rustup target add wasm32-unknown-unknown
# index.htmlファイル作成
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>egui App</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="the_canvas_id"></canvas>
</body>
</html>
EOF
# Webアプリのビルドと実行
trunk serve
# リリースビルド
cargo build --release
# 実行ファイル最適化
strip target/release/egui-app
# Windows向けクロスコンパイル
rustup target add x86_64-pc-windows-gnu
cargo build --target x86_64-pc-windows-gnu --release