mirror of
https://github.com/bevyengine/bevy
synced 2024-11-26 14:40:19 +00:00
3999365bc1
# Objective - Fix #7315 - Add IME support ## Solution - Add two new fields to `Window`, to control if IME is enabled and the candidate box position This allows the use of dead keys which are needed in French, or the full IME experience to type using Pinyin I also added a basic general text input example that can handle IME input. https://user-images.githubusercontent.com/8672791/213941353-5ed73a73-5dd1-4e66-a7d6-a69b49694c52.mp4
201 lines
6.2 KiB
Rust
201 lines
6.2 KiB
Rust
//! Simple text input support
|
|
//!
|
|
//! Return creates a new line, backspace removes the last character.
|
|
//! Clicking toggle IME (Input Method Editor) support, but the font used as limited support of characters.
|
|
//! You should change the provided font with another one to test other languages input.
|
|
|
|
use bevy::{input::keyboard::KeyboardInput, prelude::*};
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_startup_system(setup_scene)
|
|
.add_system(toggle_ime)
|
|
.add_system(listen_ime_events)
|
|
.add_system(listen_received_character_events)
|
|
.add_system(listen_keyboard_input_events)
|
|
.add_system(bubbling_text)
|
|
.run();
|
|
}
|
|
|
|
fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
commands.spawn(Camera2dBundle::default());
|
|
|
|
let font = asset_server.load("fonts/FiraMono-Medium.ttf");
|
|
|
|
commands.spawn(
|
|
TextBundle::from_sections([
|
|
TextSection {
|
|
value: "IME Enabled: ".to_string(),
|
|
style: TextStyle {
|
|
font: font.clone_weak(),
|
|
font_size: 20.0,
|
|
color: Color::WHITE,
|
|
},
|
|
},
|
|
TextSection {
|
|
value: "false\n".to_string(),
|
|
style: TextStyle {
|
|
font: font.clone_weak(),
|
|
font_size: 30.0,
|
|
color: Color::WHITE,
|
|
},
|
|
},
|
|
TextSection {
|
|
value: "IME Active: ".to_string(),
|
|
style: TextStyle {
|
|
font: font.clone_weak(),
|
|
font_size: 20.0,
|
|
color: Color::WHITE,
|
|
},
|
|
},
|
|
TextSection {
|
|
value: "false\n".to_string(),
|
|
style: TextStyle {
|
|
font: font.clone_weak(),
|
|
font_size: 30.0,
|
|
color: Color::WHITE,
|
|
},
|
|
},
|
|
TextSection {
|
|
value: "click to toggle IME, press return to start a new line\n\n".to_string(),
|
|
style: TextStyle {
|
|
font: font.clone_weak(),
|
|
font_size: 18.0,
|
|
color: Color::WHITE,
|
|
},
|
|
},
|
|
TextSection {
|
|
value: "".to_string(),
|
|
style: TextStyle {
|
|
font,
|
|
font_size: 25.0,
|
|
color: Color::WHITE,
|
|
},
|
|
},
|
|
])
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
position: UiRect {
|
|
top: Val::Px(10.0),
|
|
left: Val::Px(10.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
}),
|
|
);
|
|
|
|
commands.spawn(Text2dBundle {
|
|
text: Text::from_section(
|
|
"".to_string(),
|
|
TextStyle {
|
|
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
|
|
font_size: 100.0,
|
|
color: Color::WHITE,
|
|
},
|
|
),
|
|
..default()
|
|
});
|
|
}
|
|
|
|
fn toggle_ime(
|
|
input: Res<Input<MouseButton>>,
|
|
mut windows: Query<&mut Window>,
|
|
mut text: Query<&mut Text, With<Node>>,
|
|
) {
|
|
if input.just_pressed(MouseButton::Left) {
|
|
let mut window = windows.single_mut();
|
|
|
|
window.ime_position = window
|
|
.cursor_position()
|
|
.map(|p| Vec2::new(p.x, window.height() - p.y))
|
|
.unwrap();
|
|
window.ime_enabled = !window.ime_enabled;
|
|
|
|
let mut text = text.single_mut();
|
|
text.sections[1].value = format!("{}\n", window.ime_enabled);
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Bubble {
|
|
timer: Timer,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct ImePreedit;
|
|
|
|
fn bubbling_text(
|
|
mut commands: Commands,
|
|
mut bubbles: Query<(Entity, &mut Transform, &mut Bubble)>,
|
|
time: Res<Time>,
|
|
) {
|
|
for (entity, mut transform, mut bubble) in bubbles.iter_mut() {
|
|
if bubble.timer.tick(time.delta()).just_finished() {
|
|
commands.entity(entity).despawn();
|
|
}
|
|
transform.translation.y += time.delta_seconds() * 100.0;
|
|
}
|
|
}
|
|
|
|
fn listen_ime_events(
|
|
mut events: EventReader<Ime>,
|
|
mut status_text: Query<&mut Text, With<Node>>,
|
|
mut edit_text: Query<&mut Text, (Without<Node>, Without<Bubble>)>,
|
|
) {
|
|
for event in events.iter() {
|
|
match event {
|
|
Ime::Preedit { value, cursor, .. } if !cursor.is_none() => {
|
|
status_text.single_mut().sections[5].value = format!("IME buffer: {value}");
|
|
}
|
|
Ime::Preedit { cursor, .. } if cursor.is_none() => {
|
|
status_text.single_mut().sections[5].value = "".to_string();
|
|
}
|
|
Ime::Commit { value, .. } => {
|
|
edit_text.single_mut().sections[0].value.push_str(value);
|
|
}
|
|
Ime::Enabled { .. } => {
|
|
status_text.single_mut().sections[3].value = "true\n".to_string();
|
|
}
|
|
Ime::Disabled { .. } => {
|
|
status_text.single_mut().sections[3].value = "false\n".to_string();
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn listen_received_character_events(
|
|
mut events: EventReader<ReceivedCharacter>,
|
|
mut edit_text: Query<&mut Text, (Without<Node>, Without<Bubble>)>,
|
|
) {
|
|
for event in events.iter() {
|
|
edit_text.single_mut().sections[0].value.push(event.char);
|
|
}
|
|
}
|
|
|
|
fn listen_keyboard_input_events(
|
|
mut commands: Commands,
|
|
mut events: EventReader<KeyboardInput>,
|
|
mut edit_text: Query<(Entity, &mut Text), (Without<Node>, Without<Bubble>)>,
|
|
) {
|
|
for event in events.iter() {
|
|
match event.key_code {
|
|
Some(KeyCode::Return) => {
|
|
let (entity, text) = edit_text.single();
|
|
commands.entity(entity).insert(Bubble {
|
|
timer: Timer::from_seconds(5.0, TimerMode::Once),
|
|
});
|
|
|
|
commands.spawn(Text2dBundle {
|
|
text: Text::from_section("".to_string(), text.sections[0].style.clone()),
|
|
..default()
|
|
});
|
|
}
|
|
Some(KeyCode::Back) => {
|
|
edit_text.single_mut().1.sections[0].value.pop();
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
}
|