egui

Rustで書かれた即座に使えるGUIライブラリ。宣言的APIとリアクティブなUI更新を特徴とし、ゲーム、ツール、プロトタイピングに最適。WebAssembly対応により、ブラウザでも動作可能。

デスクトップクロスプラットフォームRustGUIimmediate-modeゲーム開発

GitHub概要

emilk/egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native

ホームページ:https://www.egui.rs/
スター25,818
ウォッチ147
フォーク1,783
作成日:2019年1月13日
言語:Rust
ライセンス:Apache License 2.0

トピックス

eguigame-developmentgamedevguiimguirustwasm

スター履歴

emilk/egui Star History
データ取得日時: 2025/7/17 10:32

フレームワーク

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