1use std::mem;
8
9use bevy::{
10 input::keyboard::{Key, KeyboardInput},
11 prelude::*,
12};
13
14fn main() {
15 App::new()
16 .add_plugins(DefaultPlugins)
17 .add_systems(Startup, setup_scene)
18 .add_systems(
19 Update,
20 (
21 toggle_ime,
22 listen_ime_events,
23 listen_keyboard_input_events,
24 bubbling_text,
25 ),
26 )
27 .run();
28}
29
30fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
31 commands.spawn(Camera2d);
32
33 let font = asset_server.load("fonts/FiraMono-Medium.ttf");
36
37 commands.spawn((
38 Text::default(),
39 Node {
40 position_type: PositionType::Absolute,
41 top: Val::Px(12.0),
42 left: Val::Px(12.0),
43 ..default()
44 },
45 children![
46 TextSpan::new("Click to toggle IME. Press return to start a new line.\n\n",),
47 TextSpan::new("IME Enabled: "),
48 TextSpan::new("false\n"),
49 TextSpan::new("IME Active: "),
50 TextSpan::new("false\n"),
51 TextSpan::new("IME Buffer: "),
52 (
53 TextSpan::new("\n"),
54 TextFont {
55 font: font.clone(),
56 ..default()
57 },
58 ),
59 ],
60 ));
61
62 commands.spawn((
63 Text2d::new(""),
64 TextFont {
65 font,
66 font_size: 100.0,
67 ..default()
68 },
69 ));
70}
71
72fn toggle_ime(
73 input: Res<ButtonInput<MouseButton>>,
74 mut window: Single<&mut Window>,
75 status_text: Single<Entity, (With<Node>, With<Text>)>,
76 mut ui_writer: TextUiWriter,
77) {
78 if input.just_pressed(MouseButton::Left) {
79 window.ime_position = window.cursor_position().unwrap();
80 window.ime_enabled = !window.ime_enabled;
81
82 *ui_writer.text(*status_text, 3) = format!("{}\n", window.ime_enabled);
83 }
84}
85
86#[derive(Component)]
87struct Bubble {
88 timer: Timer,
89}
90
91fn bubbling_text(
92 mut commands: Commands,
93 mut bubbles: Query<(Entity, &mut Transform, &mut Bubble)>,
94 time: Res<Time>,
95) {
96 for (entity, mut transform, mut bubble) in bubbles.iter_mut() {
97 if bubble.timer.tick(time.delta()).just_finished() {
98 commands.entity(entity).despawn();
99 }
100 transform.translation.y += time.delta_secs() * 100.0;
101 }
102}
103
104fn listen_ime_events(
105 mut events: EventReader<Ime>,
106 status_text: Single<Entity, (With<Node>, With<Text>)>,
107 mut edit_text: Single<&mut Text2d, (Without<Node>, Without<Bubble>)>,
108 mut ui_writer: TextUiWriter,
109) {
110 for event in events.read() {
111 match event {
112 Ime::Preedit { value, cursor, .. } if !cursor.is_none() => {
113 *ui_writer.text(*status_text, 7) = format!("{value}\n");
114 }
115 Ime::Preedit { cursor, .. } if cursor.is_none() => {
116 *ui_writer.text(*status_text, 7) = "\n".to_string();
117 }
118 Ime::Commit { value, .. } => {
119 edit_text.push_str(value);
120 }
121 Ime::Enabled { .. } => {
122 *ui_writer.text(*status_text, 5) = "true\n".to_string();
123 }
124 Ime::Disabled { .. } => {
125 *ui_writer.text(*status_text, 5) = "false\n".to_string();
126 }
127 _ => (),
128 }
129 }
130}
131
132fn listen_keyboard_input_events(
133 mut commands: Commands,
134 mut events: EventReader<KeyboardInput>,
135 edit_text: Single<(&mut Text2d, &TextFont), (Without<Node>, Without<Bubble>)>,
136) {
137 let (mut text, style) = edit_text.into_inner();
138 for event in events.read() {
139 if !event.state.is_pressed() {
141 continue;
142 }
143
144 match (&event.logical_key, &event.text) {
145 (Key::Enter, _) => {
146 if text.is_empty() {
147 continue;
148 }
149 let old_value = mem::take(&mut **text);
150
151 commands.spawn((
152 Text2d::new(old_value),
153 style.clone(),
154 Bubble {
155 timer: Timer::from_seconds(5.0, TimerMode::Once),
156 },
157 ));
158 }
159 (Key::Backspace, _) => {
160 text.pop();
161 }
162 (_, Some(inserted_text)) => {
163 if inserted_text.chars().all(is_printable_char) {
166 text.push_str(inserted_text);
167 }
168 }
169 _ => continue,
170 }
171 }
172}
173
174fn is_printable_char(chr: char) -> bool {
177 let is_in_private_use_area = ('\u{e000}'..='\u{f8ff}').contains(&chr)
178 || ('\u{f0000}'..='\u{ffffd}').contains(&chr)
179 || ('\u{100000}'..='\u{10fffd}').contains(&chr);
180
181 !is_in_private_use_area && !chr.is_ascii_control()
182}