1mod build;
2mod clipboard;
3mod edit;
4mod range;
5mod state;
6mod text;
7mod text_engine;
8mod value;
9
10pub use text::{set_input_disabled, set_input_value};
11
12use bevy::input::keyboard::{Key, KeyboardInput};
13use bevy::input_focus::InputFocus;
14use bevy::prelude::*;
15use bevy::ui::UiSystems;
16use bevy::window::Ime;
17
18pub(crate) use clipboard::InputClipboard;
19pub use clipboard::UndoStack;
20pub use edit::{PreeditState, SelectionDirection, TextEditState};
21pub(crate) use text_engine::InputTextEngine;
22
23pub struct InputPlugin;
24
25#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum InputSet {
27 Build,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum InputType {
32 #[default]
33 Text,
34 Textarea,
35 Number,
36 Range,
37 Checkbox,
38 Radio,
39 Password,
40}
41
42#[derive(Component, Debug, Clone)]
44pub struct AddInput {
45 pub name: String,
46 pub input_type: InputType,
47 pub value: String,
48 pub checked: bool,
49 pub placeholder: String,
50 pub size_chars: Option<usize>,
51 pub rows: Option<usize>,
52 pub min: Option<f32>,
53 pub max: Option<f32>,
54 pub step: Option<f32>,
55 pub class: Option<String>,
56 pub text_class: Option<String>,
57 pub disabled: bool,
58}
59
60impl Default for AddInput {
61 fn default() -> Self {
62 Self {
63 name: String::new(),
64 input_type: InputType::Text,
65 value: String::new(),
66 checked: false,
67 placeholder: String::new(),
68 size_chars: None,
69 rows: None,
70 min: None,
71 max: None,
72 step: None,
73 class: None,
74 text_class: None,
75 disabled: false,
76 }
77 }
78}
79
80#[derive(Component, Debug, Clone)]
81pub struct InputField {
82 pub name: String,
83 pub input_type: InputType,
84 pub checked: bool,
85 pub placeholder: String,
86 pub viewport_entity: Option<Entity>,
87 pub text_entity: Option<Entity>,
88 pub selection_entity: Option<Entity>,
89 pub caret_entity: Option<Entity>,
90 pub edit_state: TextEditState,
91 pub initial_value: String,
92 pub initial_checked: bool,
93 pub min: Option<f32>,
94 pub max: Option<f32>,
95 pub step: Option<f32>,
96 pub caret_blink_resume_at: f64,
97 pub preferred_caret_x: Option<f32>,
98 pub undo_stack: UndoStack,
99}
100
101impl InputField {
102 pub fn submitted_value(&self) -> String {
103 match self.input_type {
104 InputType::Checkbox => self.checked.to_string(),
105 InputType::Radio => self.value().to_string(),
106 _ => self.value().to_string(),
107 }
108 }
109}
110
111#[derive(Component, Debug, Clone)]
112pub(crate) struct RangeState {
113 pub track: Entity,
114 pub fill: Entity,
115 pub thumb: Entity,
116 pub drag_start_value: f32,
117}
118
119impl InputField {
120 pub fn value(&self) -> &str {
121 self.edit_state.committed()
122 }
123
124 pub fn set_value(&mut self, value: impl Into<String>) {
125 self.edit_state.set_text(value);
126 }
127
128 pub fn numeric_value(&self) -> Option<f32> {
129 value::parse_number_buffer(self.value())
130 }
131
132 pub fn is_multiline(&self) -> bool {
133 matches!(self.input_type, InputType::Textarea)
134 }
135
136 pub fn is_text_like(&self) -> bool {
137 matches!(
138 self.input_type,
139 InputType::Text
140 | InputType::Textarea
141 | InputType::Number
142 | InputType::Range
143 | InputType::Password
144 )
145 }
146
147 pub fn is_checkable(&self) -> bool {
148 matches!(self.input_type, InputType::Checkbox | InputType::Radio)
149 }
150 pub fn is_toggle(&self) -> bool {
151 matches!(self.input_type, InputType::Checkbox | InputType::Radio)
152 }
153
154 pub fn reset(&mut self) {
155 self.edit_state.set_text(self.initial_value.clone());
156 self.checked = self.initial_checked;
157 }
158 pub fn step_by(&mut self, direction: f32) -> Option<String> {
159 if !matches!(self.input_type, InputType::Number | InputType::Range) || direction == 0.0 {
160 return None;
161 }
162 let current = self
163 .numeric_value()
164 .unwrap_or_else(|| self.min.unwrap_or(0.0));
165 let step = self.step.unwrap_or(1.0);
166 let next = value::snap_numeric_value(
167 current + step * direction.signum(),
168 self.min,
169 self.max,
170 self.step,
171 );
172 let next_value = value::format_numeric_value(next, self.step);
173 if self.value() == next_value {
174 return None;
175 }
176 self.set_value(next_value.clone());
177 Some(next_value)
178 }
179}
180
181#[derive(Component, Debug, Clone, Copy)]
182pub struct InputText;
183
184#[allow(dead_code)]
185#[derive(Component, Debug, Clone, Copy)]
186pub(crate) struct InputIndicator;
187
188#[derive(Component, Debug, Clone, Copy)]
189pub(crate) struct InputViewport;
190
191#[derive(Component, Debug, Clone, Copy, Default)]
192pub struct DisabledInput;
193
194#[derive(Component, Debug, Clone, Copy)]
195pub struct InputSelection;
196
197#[derive(Component, Debug, Clone, Copy)]
198pub(crate) struct InputSelectionSegment;
199
200#[derive(Resource, Debug, Default)]
201pub(crate) struct SelectionSegmentPool {
202 pub available: usize,
203 pub max_needed: usize,
204}
205
206#[derive(Component, Debug, Clone, Copy)]
207pub struct InputCaret;
208
209#[allow(dead_code)]
210#[derive(Component, Debug, Clone, Copy)]
211pub(crate) struct InputCheckRoot;
212
213#[derive(Component, Debug, Clone, Copy)]
214pub struct InputCursorPosition {
215 pub x: f32,
216 pub y: f32,
217}
218
219#[derive(Component, Debug, Clone, Copy, Default)]
220pub(crate) struct InputScrollOffset {
221 pub x: f32,
222 pub y: f32,
223}
224
225#[derive(Component, Debug, Clone, Copy, Default)]
226pub(crate) struct InputClickState {
227 pub last_click_time: f64,
228 pub click_count: u8,
229 pub last_click_position: Option<Vec2>,
230}
231
232#[derive(Component, Debug, Clone, Copy)]
233pub(crate) struct RangeTrack {
234 pub input: Entity,
235}
236
237#[derive(Component, Debug, Clone, Copy)]
238pub(crate) struct RangeFill;
239
240#[derive(Component, Debug, Clone, Copy)]
241pub(crate) struct RangeThumb;
242
243#[derive(Component, Debug, Clone, Copy)]
244pub(crate) struct ToggleIndicator;
245
246#[derive(Component, Debug, Clone, Copy)]
247pub(crate) struct ToggleFill;
248
249#[derive(Component, Debug, Clone, Copy)]
250pub(crate) struct CheckboxMark;
251
252#[derive(Message, Debug, Clone)]
253pub struct InputValueChangedMessage {
254 pub entity: Entity,
255 pub name: String,
256 pub value: String,
257 pub runtime_value: InputRuntimeValue,
258}
259
260#[derive(Message, Debug, Clone)]
261pub struct InputSubmitMessage {
262 pub entity: Entity,
263 pub name: String,
264}
265
266impl Plugin for InputPlugin {
267 fn build(&self, app: &mut App) {
268 app.add_message::<InputValueChangedMessage>()
269 .add_message::<InputSubmitMessage>()
270 .add_message::<KeyboardInput>()
271 .add_message::<Ime>()
272 .add_message::<Pointer<Click>>()
273 .init_resource::<InputFocus>()
274 .init_resource::<SelectionSegmentPool>()
275 .init_resource::<InputTextEngine>()
276 .insert_non_send_resource(InputClipboard::new())
277 .add_systems(
278 Update,
279 (
280 build::add_input.in_set(InputSet::Build),
281 build::sync_toggle_visuals,
282 state::sync_radio_groups,
283 state::clear_input_focus_on_foreign_click,
284 state::sync_input_focus_visuals,
285 range::sync_range_visuals,
286 ),
287 )
288 .add_systems(
289 Update,
290 (
291 state::handle_keyboard_input,
292 state::handle_ime_input,
293 state::sync_input_ime_state,
294 ),
295 )
296 .add_systems(
297 PostUpdate,
298 state::sync_input_edit_visuals.after(UiSystems::PostLayout),
299 );
300 }
301}
302
303fn active_input_entity(
304 input_focus: &InputFocus,
305 fields: &Query<(), With<InputField>>,
306) -> Option<Entity> {
307 input_focus.get().filter(|entity| fields.contains(*entity))
308}
309
310fn set_input_focus(input_focus: &mut InputFocus, entity: Entity) {
311 input_focus.set(entity);
312}
313
314fn clear_input_focus(input_focus: &mut InputFocus) {
315 input_focus.clear();
316}
317
318pub(crate) fn push_value_changed(
319 value_changed: &mut MessageWriter<InputValueChangedMessage>,
320 entity: Entity,
321 field: &InputField,
322) {
323 value_changed.write(InputValueChangedMessage {
324 entity,
325 name: field.name.clone(),
326 value: field.value().to_string(),
327 runtime_value: input_runtime_value(field),
328 });
329}
330
331pub(crate) fn push_range_value_changed(
332 value_changed: &mut MessageWriter<InputValueChangedMessage>,
333 entity: Entity,
334 name: &str,
335 value: String,
336) {
337 let runtime_value = value
338 .parse::<f64>()
339 .map(InputRuntimeValue::Number)
340 .unwrap_or_else(|_| InputRuntimeValue::Text(value.clone()));
341 value_changed.write(InputValueChangedMessage {
342 entity,
343 name: name.to_string(),
344 value,
345 runtime_value,
346 });
347}
348
349#[derive(Debug, Clone, PartialEq)]
350pub enum InputRuntimeValue {
351 Text(String),
352 Bool(bool),
353 Number(f64),
354}
355
356pub(crate) fn input_runtime_value(field: &InputField) -> InputRuntimeValue {
357 match field.input_type {
358 InputType::Checkbox => InputRuntimeValue::Bool(field.checked),
359 InputType::Radio => InputRuntimeValue::Text(field.value().to_string()),
360 InputType::Number | InputType::Range => field
361 .numeric_value()
362 .map(|value| InputRuntimeValue::Number(value as f64))
363 .unwrap_or_else(|| InputRuntimeValue::Text(field.value().to_string())),
364 InputType::Text | InputType::Textarea | InputType::Password => {
365 InputRuntimeValue::Text(field.value().to_string())
366 }
367 }
368}
369
370fn is_printable_char(chr: char) -> bool {
371 let is_in_private_use_area = ('\u{e000}'..='\u{f8ff}').contains(&chr);
372 !chr.is_control() && !is_in_private_use_area
373}
374
375fn key_is_submit(key: &Key) -> bool {
376 matches!(key, Key::Enter)
377}
378
379fn sync_window_ime(primary_window: &mut Window, enabled: bool, position: Vec2) {
380 primary_window.ime_enabled = enabled;
381 primary_window.ime_position = position;
382}