LoginSignup
4
3

Bevy + eguiで日本語入力に対応させる

Last updated at Posted at 2023-08-01

なぜこの記事を書こうと思ったのか

Rustでゲームを作ろうと思った時にまず選択肢にあがるcrateはBevyだと思うのですが、GUI部分がどうしても機能不足だと感じることが多かったです。

その問題を解決するために補助的にeguiを使うことが多いのですが、幸い、すでにBevy内でeguiを使えるようにするcrateが存在しております。
ただここで問題が生じます。

そうです。日本語入力に対応しておりませんでした:;

今回の記事は、タイトルにある通り Bevy + eguiの組み合わせで日本語入力出来るようにするための記事です。Bevyでゲームを作っている人には参考になるかもしれません。

完成形のサンプルプロジェクトは こちら です

Cargo.tomlとmain.rsで文字入力できるようにする

Cargo.tomlとmain.rsで文字入力できるようにする

Cargo.toml
[package]
name = "sample_bevy_egui"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.11.0"
bevy_egui = "0.21.0"

その後main.rsでtop_panel部分にTextEditを追加します。

main.rs

use bevy_egui::{egui, EguiContexts, EguiPlugin};
use bevy::prelude::*;

#[derive(Resource)] 
pub struct MyApp{
    pub txt: String,
}
impl Default for MyApp{
    fn default() -> Self{
        MyApp{
            txt: String::new(),
        }
    }
}

fn main() {
    App::new()    
    .add_plugins(DefaultPlugins.set(WindowPlugin {
        primary_window: Some(Window {
            position: WindowPosition::new(IVec2::new( 400, 200)),
            resolution: (220.0, 100.0).into(),
            ..default()
        }),
        ..default()
    }))

    .add_plugins(EguiPlugin) 
    .insert_resource(MyApp::default())
    .add_systems(Update, 
        (
            ui_system,
        ) 
    )      
    .run();
}

pub fn ui_system(
    mut contexts: EguiContexts, 
    mut app: ResMut<MyApp>, 
) {
    egui::TopBottomPanel::top("top_panel").show(contexts.ctx_mut(), |ui|{
        ui.text_edit_singleline(&mut app.txt);
    });
}

cargo runでビルド。

ただこのままだとアルファベットや数字は入力できても日本語入力ができません。。

とりあえずime.rsというファイルをsrcの直下に作成します。

src
L main.rs
L ime.rs
ime.rs
use bevy_egui::egui;
use bevy::prelude::*;

#[derive()]
pub struct ImeGroup{
    pub ime_txt: ImeText,
}
impl Default for ImeGroup{
    fn default() -> Self{
        ImeGroup { 
            ime_txt: ImeText::default(),
        }
    }
}
#[derive()]
pub struct ImeText{
    pub text: String,
    pub ime_string: String,
    pub ime_string_index: usize,
    pub cursor_index: usize,
    pub is_ime_input: bool,
    pub is_focus: bool,
    pub is_ime: bool,
    pub is_cursor_move: bool,
}
impl Default for ImeText{
    fn default() -> Self{
        ImeText{
            text: String::from(""),
            ime_string: String::from(""),
            ime_string_index: 0,
            cursor_index: 0,
            is_ime_input: false,
            is_focus: false,
            is_ime: false,
            is_cursor_move: true,
        }
    }
}

pub fn get_texteditoutput(ui: &mut egui::Ui, ime: &mut ImeText, width: f32) -> egui::text_edit::TextEditOutput{
    let mut lyt = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
        let tmp = get_layoutjob(string, ime);
        ui.fonts(|f| f.layout_job(tmp))
    };
    let mut tmp_text = match ime.ime_string.len(){
        0 => {ime.text.to_string()},
        _ => {
            let mut front = String::new();
            let mut back = String::new();
            let mut cnt = 0;
            for c in ime.text.chars(){
                if cnt < ime.cursor_index{ front.push_str(&c.to_string()); } 
                else{ back.push_str(&c.to_string()); }
                cnt += 1;
            }                 
            format!("{}{}{}", front, ime.ime_string, back)
        }
    };
    let mut te1 = egui::TextEdit::singleline(&mut tmp_text).desired_width(width).layouter(&mut lyt).show(ui);
    ime.is_focus = te1.response.has_focus();
    if !ime.is_ime {ime.text = tmp_text.to_string();}
    if te1.cursor_range.is_some(){ 
        ime.cursor_index = te1.cursor_range.unwrap().secondary.rcursor.column; 
    }
    if ime.is_ime {
        
    }
    if ime.is_ime_input{ 
        ime.is_ime_input = false;

        if ime.is_cursor_move{
            let mut res_cursor = te1.cursor_range.unwrap().primary.clone();
            for _ in 0..ime.ime_string_index{
                res_cursor = te1.galley.cursor_right_one_character(&res_cursor);
            }
            let cr = egui::text_edit::CursorRange{
                primary: res_cursor,
                secondary: res_cursor,
            };
            te1.state.set_cursor_range(Some(cr));
        }
    }
    if !ime.is_cursor_move{
        ime.is_cursor_move = true;
    }
    te1
}

fn get_layoutjob(string: &str, ime: &ImeText) -> egui::text::LayoutJob{
    let tmp = match ime.is_ime{
        false => { egui::text::LayoutJob::simple_singleline(string.into(),egui::FontId::default(), egui::Color32::WHITE) },
        _ => {
            let mut front = String::new();
            let mut back = String::new();
            let mut cnt = 0;
            for c in ime.text.chars(){
                if cnt < ime.cursor_index{ front.push_str(&c.to_string()); } 
                else{ back.push_str(&c.to_string()); }
                cnt += 1;
            }

            let mut lss:Vec<egui::text::LayoutSection> = vec![];
            let mut f_cnt = 0;
            let mut b_cnt = 0;
            b_cnt = b_cnt + front.len();
            let ls_front = egui::text::LayoutSection {
                leading_space: 0.0,
                byte_range: f_cnt..b_cnt,
                format: egui::TextFormat {
                    color: egui::Color32::WHITE,
                    ..Default::default()
                },
            };
            lss.push(ls_front);
            f_cnt = b_cnt;

            b_cnt = b_cnt + ime.ime_string.len();
            let ls_text = egui::text::LayoutSection {
                leading_space: 0.0,
                byte_range: f_cnt..b_cnt,
                format: egui::TextFormat {
                    color: egui::Color32::GREEN,
                    background: egui::Color32::from_rgb(0, 128, 64),
                    ..Default::default()
                },
            };
            lss.push(ls_text);
            f_cnt = b_cnt;

            b_cnt = b_cnt + back.len();
            let ls_back = egui::text::LayoutSection {
                leading_space: 0.0,
                byte_range: f_cnt..b_cnt,
                format: egui::TextFormat {
                    color: egui::Color32::WHITE,
                    ..Default::default()
                },
            };
            lss.push(ls_back);

            egui::text::LayoutJob {
                sections: lss,
                text: format!("{}{}{}",front, ime.ime_string, back),        
                ..Default::default()
            }
        }
    };
    tmp
}

pub fn listen_ime_events(
    mut events: EventReader<Ime>,
    mut app: ResMut<super::MyApp>, 
) {
    for event in events.iter() {
        match event {
            Ime::Preedit { value, cursor, .. } if cursor.is_some() => {
                if app.ime.ime_txt.is_focus{ 
                    app.ime.ime_txt.ime_string = value.to_string();
                    app.ime.ime_txt.ime_string_index = app.ime.ime_txt.ime_string.chars().count();
                }
            }
            Ime::Preedit { cursor, .. } if cursor.is_none() => {
                
            }
            Ime::Commit { value,.. } => {
                if value.is_empty(){
                    app.ime.ime_txt.is_cursor_move = false;
                }      
                if app.ime.ime_txt.is_focus{
                    let tmp = value.to_string();
                    if app.ime.ime_txt.text.chars().count() == app.ime.ime_txt.cursor_index{
                        app.ime.ime_txt.text.push_str(&tmp);
                    }else{
                        let mut front = String::new();
                        let mut back = String::new();
                        let mut cnt = 0;
                        for c in app.ime.ime_txt.text.chars(){
                            if cnt < app.ime.ime_txt.cursor_index{ front.push_str(&c.to_string()); } 
                            else{ back.push_str(&c.to_string()); }
                            cnt += 1;
                        }                 
                        app.ime.ime_txt.text = format!("{}{}{}", front, tmp, back);
                    }
                    app.ime.ime_txt.is_ime_input = true;
                    app.ime.ime_txt.ime_string = String::new();
                }                
            }
            Ime::Enabled { .. } => { 
                app.ime.ime_txt.is_ime = true;
            }
            Ime::Disabled { .. } => { 
                app.ime.ime_txt.is_ime = false;
            }
            _ => (),
        }
    }
}

そしてmain.rsを次のように修正します。

main.rs
use bevy_egui::{egui, EguiContexts, EguiPlugin};
use bevy::prelude::*;

mod ime;

#[derive(Resource)] 
pub struct MyApp{
    pub txt: String,
    pub ime: ime::ImeGroup,
}
impl Default for MyApp{
    fn default() -> Self{
        MyApp{
            txt: String::new(),
            ime: ime::ImeGroup::default(),
        }
    }
}

fn main() {
    App::new()    
    .add_plugins(DefaultPlugins.set(WindowPlugin {
        primary_window: Some(Window {
            position: WindowPosition::new(IVec2::new( 400, 200)),
            resolution: (220.0, 100.0).into(),
            ..default()
        }),
        ..default()
    }))

    .add_plugins(EguiPlugin) 
    .insert_resource(MyApp::default())
    .add_systems(Startup, setup_system)
    .add_systems(Update, 
        (
            ui_system,
            ime::listen_ime_events
        ) 
    )      
    .run();
}

pub fn setup_system(
    mut egui_context: EguiContexts,
    mut windows: Query<&mut Window>
) {

    let mut window = windows.single_mut();
    window.ime_enabled = true;
    let mut txt_font = egui::FontDefinitions::default();
    txt_font.families.get_mut(&egui::FontFamily::Proportional).unwrap().insert(0, "Meiryo".to_owned());
    let fd = egui::FontData::from_static(include_bytes!("C:/Windows/Fonts/Meiryo.ttc"));
    txt_font.font_data.insert("Meiryo".to_owned(), fd);
    egui_context.ctx_mut().set_fonts(txt_font); 
}

pub fn ui_system(
    mut contexts: EguiContexts, 
    mut app: ResMut<MyApp>, 
) {
    let ctx = contexts.ctx_mut();
    egui::TopBottomPanel::top("top_panel").show(ctx, |ui|{
        let teo = ime::get_texteditoutput(ui, &mut app.as_mut().ime.ime_txt, 200.0);
        teo.state.store(ctx, teo.response.id);
        app.txt = app.ime.ime_txt.text.to_string();
    });
}

解説

ime.rsのlisten_ime_events関数

match event で 日本語入力等を監視して、変数に格納しております。

ime.rsのget_texteditoutput関数

match event で 日本語入力等を監視して、変数に格納しております。

結果

cargo run でビルド。無事に日本語入力に対応できました。
test_230801.gif

参考にさせていただいたサイト

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3