1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5
6use smol_str::SmolStr;
7use ui_layout::Style;
8
9use crate::engine::bridge::text::{self, TextMeasureRequest};
10use crate::engine::layouter::types::{Color, TextFlowStyle, TextStyle};
11use crate::engine::renderer_model::{Brush, DrawCommand, FillRule, Paint, rect_path};
12use crate::engine::ui::components::input_text_types::{
13 InputTextEvent, InputTextKey, InputTextState,
14};
15use crate::engine::ui::custom_node::{ContentSize, CustomNode};
16
17pub type OnValueChange = dyn Fn(&str) + Send + Sync;
19
20const INLINE_PADDING: f32 = 4.0;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24struct EditSnapshot {
25 value: String,
26 caret: usize,
27 preedit: String,
28}
29
30pub struct InputTextComponent {
32 state: Mutex<InputTextState>,
33 placeholder: SmolStr,
34 measurer: Arc<dyn text::TextMeasurer>,
35 undo_stack: Mutex<Vec<EditSnapshot>>,
36 redo_stack: Mutex<Vec<EditSnapshot>>,
37 dirty: AtomicBool,
38 on_value_change: Option<Arc<OnValueChange>>,
39 on_enter: Option<Arc<OnValueChange>>,
40}
41
42impl std::fmt::Debug for InputTextComponent {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("InputTextComponent")
45 .field("state", &self.state.lock().unwrap())
46 .field("placeholder", &self.placeholder)
47 .finish_non_exhaustive()
48 }
49}
50
51impl InputTextComponent {
52 pub fn new(
54 value: impl Into<String>,
55 placeholder: impl Into<SmolStr>,
56 measurer: Arc<dyn text::TextMeasurer>,
57 ) -> Self {
58 let value = value.into();
59 let caret = value.len();
60 Self {
61 state: Mutex::new(InputTextState {
62 value,
63 preedit: String::new(),
64 caret,
65 focused: false,
66 }),
67 placeholder: placeholder.into(),
68 measurer,
69 undo_stack: Mutex::new(Vec::new()),
70 redo_stack: Mutex::new(Vec::new()),
71 dirty: AtomicBool::new(true),
72 on_value_change: None,
73 on_enter: None,
74 }
75 }
76
77 pub fn with_on_change(
79 value: impl Into<String>,
80 placeholder: impl Into<SmolStr>,
81 measurer: Arc<dyn text::TextMeasurer>,
82 on_value_change: Arc<OnValueChange>,
83 ) -> Self {
84 let mut input = Self::new(value, placeholder, measurer);
85 input.on_value_change = Some(on_value_change);
86 input
87 }
88
89 pub fn with_on_enter(
91 value: impl Into<String>,
92 placeholder: impl Into<SmolStr>,
93 measurer: Arc<dyn text::TextMeasurer>,
94 on_enter: Arc<OnValueChange>,
95 ) -> Self {
96 let mut input = Self::new(value, placeholder, measurer);
97 input.on_enter = Some(on_enter);
98 input
99 }
100
101 pub fn set_value(&self, value: impl Into<String>) {
105 let value = value.into();
106 let mut state = self.state.lock().unwrap();
107 state.value = value;
108 state.caret = state.value.len();
109 state.preedit.clear();
110 self.dirty.store(true, Ordering::Relaxed);
111 }
112
113 pub fn state(&self) -> InputTextState {
115 self.state.lock().unwrap().clone()
116 }
117
118 fn previous_boundary(value: &str, caret: usize) -> usize {
119 value[..caret]
120 .char_indices()
121 .next_back()
122 .map_or(0, |(index, _)| index)
123 }
124
125 fn next_boundary(value: &str, caret: usize) -> usize {
126 value[caret..]
127 .char_indices()
128 .nth(1)
129 .map_or(value.len(), |(offset, _)| caret + offset)
130 }
131
132 fn handle_key(state: &mut InputTextState, key: InputTextKey) {
133 match key {
134 InputTextKey::Backspace if state.caret > 0 => {
135 let previous = Self::previous_boundary(&state.value, state.caret);
136 state.value.replace_range(previous..state.caret, "");
137 state.caret = previous;
138 }
139 InputTextKey::Delete if state.caret < state.value.len() => {
140 let next = Self::next_boundary(&state.value, state.caret);
141 state.value.replace_range(state.caret..next, "");
142 }
143 InputTextKey::Left => {
144 state.caret = Self::previous_boundary(&state.value, state.caret);
145 }
146 InputTextKey::Right => {
147 state.caret = Self::next_boundary(&state.value, state.caret);
148 }
149 InputTextKey::Home => state.caret = 0,
150 InputTextKey::End => state.caret = state.value.len(),
151 InputTextKey::Backspace | InputTextKey::Delete => {}
152 }
153 }
154
155 fn snapshot(&self) -> EditSnapshot {
156 let state = self.state.lock().unwrap();
157 EditSnapshot {
158 value: state.value.clone(),
159 caret: state.caret,
160 preedit: state.preedit.clone(),
161 }
162 }
163
164 fn push_undo(&self, snapshot: EditSnapshot) {
165 self.undo_stack.lock().unwrap().push(snapshot);
166 self.redo_stack.lock().unwrap().clear();
167 }
168
169 fn undo(&self) {
170 let Some(snapshot) = self.undo_stack.lock().unwrap().pop() else {
171 return;
172 };
173 self.redo_stack.lock().unwrap().push(self.snapshot());
174 let mut state = self.state.lock().unwrap();
175 state.value = snapshot.value;
176 state.caret = snapshot.caret;
177 state.preedit = snapshot.preedit;
178 self.dirty.store(true, Ordering::Relaxed);
179 }
180
181 fn redo(&self) {
182 let Some(snapshot) = self.redo_stack.lock().unwrap().pop() else {
183 return;
184 };
185 self.undo_stack.lock().unwrap().push(self.snapshot());
186 let mut state = self.state.lock().unwrap();
187 state.value = snapshot.value;
188 state.caret = snapshot.caret;
189 state.preedit = snapshot.preedit;
190 self.dirty.store(true, Ordering::Relaxed);
191 }
192}
193
194impl CustomNode for InputTextComponent {
195 fn draw_sized(
196 &self,
197 cmd_buf: &mut Vec<DrawCommand>,
198 text_style: &TextStyle,
199 text_flow_style: &TextFlowStyle,
200 _style: &Style,
201 size: ContentSize,
202 ) {
203 cmd_buf.push(DrawCommand::Fill {
204 path: rect_path(0.0, 0.0, size.width, size.height),
205 rule: FillRule::NonZero,
206 paint: Paint {
207 brush: Brush::Solid(Color(255, 255, 255, 255)),
208 opacity: 1.0,
209 },
210 });
211
212 let state = self.state.lock().unwrap();
213 let mut style = text_style.clone();
214 let (display_text, placeholder) = if state.value.is_empty() && state.preedit.is_empty() {
215 (self.placeholder.as_str().to_owned(), true)
216 } else {
217 (
218 format!(
219 "{}{}{}",
220 &state.value[..state.caret],
221 state.preedit,
222 &state.value[state.caret..]
223 ),
224 false,
225 )
226 };
227 if placeholder {
228 style.color = Color(128, 128, 128, 255);
229 }
230
231 let preedit =
232 (!state.preedit.is_empty()).then_some((state.caret, state.caret + state.preedit.len()));
233 let caret = state.focused.then_some(state.caret + state.preedit.len());
234
235 draw_text_input(
236 &*self.measurer,
237 cmd_buf,
238 display_text,
239 INLINE_PADDING,
240 ((size.height - text_flow_style.font_size) * 0.5).max(0.0),
241 &style,
242 text_flow_style,
243 caret,
244 preedit,
245 text_style.color,
246 INLINE_PADDING,
247 (size.height - INLINE_PADDING * 2.0).max(0.0),
248 (size.height - 3.0).max(0.0),
249 );
250 }
251
252 fn intrinsic_size(&self) -> ContentSize {
253 ContentSize {
254 width: 200.0,
255 height: 28.0,
256 }
257 }
258
259 fn accepts_text_input(&self) -> bool {
260 true
261 }
262
263 fn set_focused(&self, focused: bool) {
264 let mut state = self.state.lock().unwrap();
265 state.focused = focused;
266 self.dirty.store(true, Ordering::Relaxed);
267 if !focused {
268 state.preedit.clear();
269 }
270 }
271
272 fn is_focused(&self) -> bool {
273 self.state.lock().unwrap().focused
274 }
275
276 fn handle_text_input(&self, event: InputTextEvent) -> bool {
277 match event {
278 InputTextEvent::Insert(text)
279 | InputTextEvent::Commit(text)
280 | InputTextEvent::Paste(text) => {
281 let text: String = text
282 .chars()
283 .filter(|character| !character.is_control())
284 .collect();
285 if text.is_empty() {
286 return true;
287 }
288 self.push_undo(self.snapshot());
289 let mut state = self.state.lock().unwrap();
290 let caret = state.caret;
291 state.value.insert_str(caret, &text);
292 state.caret += text.len();
293 state.preedit.clear();
294 let value = state.value.clone();
295 drop(state);
296 self.dirty.store(true, Ordering::Relaxed);
297 if let Some(ref cb) = self.on_value_change {
298 cb(&value);
299 }
300 }
301 InputTextEvent::Preedit(text) => {
302 let mut state = self.state.lock().unwrap();
303 if state.preedit != text {
304 state.preedit = text;
305 self.dirty.store(true, Ordering::Relaxed);
306 }
307 }
308 InputTextEvent::Key(key) => {
309 let changed = matches!(key, InputTextKey::Backspace | InputTextKey::Delete);
310 if changed {
311 self.push_undo(self.snapshot());
312 }
313 let mut state = self.state.lock().unwrap();
314 state.preedit.clear();
315 Self::handle_key(&mut state, key);
316 let value = state.value.clone();
317 drop(state);
318 self.dirty.store(true, Ordering::Relaxed);
319 if changed && let Some(ref cb) = self.on_value_change {
320 cb(&value);
321 }
322 }
323 InputTextEvent::Enter => {
324 let mut state = self.state.lock().unwrap();
325 if !state.preedit.is_empty() {
326 state.preedit.clear();
327 self.dirty.store(true, Ordering::Relaxed);
328 }
329 let value = state.value.clone();
330 drop(state);
331 if let Some(ref cb) = self.on_enter {
332 cb(&value);
333 }
334 }
335 InputTextEvent::Undo => {
336 self.undo();
337 }
338 InputTextEvent::Redo => {
339 self.redo();
340 }
341 InputTextEvent::CancelComposition => {
342 let mut state = self.state.lock().unwrap();
343 if !state.preedit.is_empty() {
344 state.preedit.clear();
345 self.dirty.store(true, Ordering::Relaxed);
346 }
347 }
348 }
349 true
350 }
351
352 fn is_composing(&self) -> bool {
353 !self.state.lock().unwrap().preedit.is_empty()
354 }
355
356 fn needs_repaint(&self) -> bool {
357 self.dirty.swap(false, Ordering::Relaxed)
358 }
359
360 fn composition_rect(&self) -> Option<(f32, f32, f32, f32)> {
361 let state = self.state.lock().unwrap();
362 if state.preedit.is_empty() {
363 return None;
364 }
365 let display_text = format!(
366 "{}{}{}",
367 &state.value[..state.caret],
368 state.preedit,
369 &state.value[state.caret..]
370 );
371 let style = TextStyle::default();
372 let Ok(fragments) = self.measurer.measure(&TextMeasureRequest {
373 text: display_text,
374 attribute: text::TextAttribute {
375 style,
376 flow_style: TextFlowStyle::default(),
377 },
378 }) else {
379 return None;
380 };
381 let mut byte_offset = 0;
382 let mut width = 0.0;
383 let mut start_x = None;
384 let mut preedit_width = 0.0;
385 let preedit_start = state.caret;
386 let preedit_end = state.caret + state.preedit.len();
387 for fragment in &fragments {
388 let frag_start = byte_offset;
389 let frag_end = byte_offset + fragment.text.len();
390 if start_x.is_none() && frag_start >= preedit_start {
391 start_x = Some(width);
392 }
393 if frag_end <= preedit_end && frag_start < preedit_end {
394 preedit_width += fragment.width;
395 }
396 byte_offset = frag_end;
397 width += fragment.width;
398 }
399 let start_x = start_x.unwrap_or(width);
400 Some((start_x, 0.0, preedit_width.max(0.0), 1.0))
401 }
402
403 fn role(&self) -> Option<&'static str> {
404 Some("textbox")
405 }
406
407 fn label(&self) -> Option<String> {
408 (!self.placeholder.is_empty()).then(|| self.placeholder.to_string())
409 }
410
411 fn value(&self) -> Option<String> {
412 Some(self.state.lock().unwrap().value.clone())
413 }
414}
415
416#[allow(clippy::too_many_arguments)]
418fn draw_text_input(
419 measurer: &dyn text::TextMeasurer,
420 cmd_buf: &mut Vec<DrawCommand>,
421 text: String,
422 x: f32,
423 y: f32,
424 style: &TextStyle,
425 flow_style: &TextFlowStyle,
426 caret: Option<usize>,
427 preedit: Option<(usize, usize)>,
428 decoration_color: Color,
429 caret_top: f32,
430 caret_height: f32,
431 underline_y: f32,
432) {
433 let Ok(fragments) = measurer.measure(&TextMeasureRequest {
434 text: text.clone(),
435 attribute: text::TextAttribute {
436 style: style.clone(),
437 flow_style: *flow_style,
438 },
439 }) else {
440 return;
441 };
442
443 let mut caret_x = None;
444 let mut preedit_start_x = None;
445 let mut preedit_end_x = None;
446
447 let mut byte_offset = 0;
448 let mut width = 0.0;
449
450 for fragment in &fragments {
451 if let Some(caret_pos) = caret
452 && caret_x.is_none()
453 && caret_pos <= byte_offset
454 {
455 caret_x = Some(width);
456 }
457
458 if let Some((start, end)) = preedit {
459 if preedit_start_x.is_none() && start <= byte_offset {
460 preedit_start_x = Some(width);
461 }
462
463 if preedit_end_x.is_none() && end <= byte_offset {
464 preedit_end_x = Some(width);
465 }
466 }
467
468 byte_offset += fragment.text.len();
469 width += fragment.width;
470 }
471
472 if let Some(caret_pos) = caret
473 && caret_x.is_none()
474 && caret_pos <= byte_offset
475 {
476 caret_x = Some(width);
477 }
478
479 if let Some((start, end)) = preedit {
480 if preedit_start_x.is_none() && start <= byte_offset {
481 preedit_start_x = Some(width);
482 }
483
484 if preedit_end_x.is_none() && end <= byte_offset {
485 preedit_end_x = Some(width);
486 }
487 }
488
489 let paint = Paint {
490 brush: Brush::Solid(decoration_color),
491 opacity: 1.0,
492 };
493
494 if let (Some(start_x), Some(end_x)) = (preedit_start_x, preedit_end_x) {
495 cmd_buf.push(DrawCommand::Fill {
496 path: rect_path(x + start_x, underline_y, (end_x - start_x).max(0.0), 1.0),
497 rule: FillRule::NonZero,
498 paint: paint.clone(),
499 });
500 }
501
502 if let Some(caret_x) = caret_x {
503 cmd_buf.push(DrawCommand::Fill {
504 path: rect_path(x + caret_x, caret_top, 1.0, caret_height),
505 rule: FillRule::NonZero,
506 paint,
507 });
508 }
509
510 cmd_buf.push(DrawCommand::DrawText {
511 x,
512 y,
513 text: text.into(),
514 style: style.clone(),
515 flow_style: *flow_style,
516 });
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 use crate::engine::bridge::text::FallbackTextMeasurer;
524
525 fn make_component(value: &str, placeholder: &str) -> InputTextComponent {
526 InputTextComponent::new(value, placeholder, Arc::new(FallbackTextMeasurer))
527 }
528
529 #[test]
530 fn preedit_is_replaced_by_ime_commit() {
531 let input = make_component("abc", "");
532 input.handle_text_input(InputTextEvent::Preedit("にほ".into()));
533 assert_eq!(input.state().preedit, "にほ");
534
535 input.handle_text_input(InputTextEvent::Commit("日本".into()));
536 let state = input.state();
537 assert_eq!(state.value, "abc日本");
538 assert!(state.preedit.is_empty());
539 }
540
541 #[test]
542 fn editing_uses_utf8_character_boundaries() {
543 let input = make_component("a日b", "");
544 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
545 input.handle_text_input(InputTextEvent::Key(InputTextKey::Backspace));
546 assert_eq!(input.state().value, "ab");
547 }
548
549 #[test]
550 fn undo_redo_restores_value_and_caret() {
551 let input = make_component("", "");
552 input.handle_text_input(InputTextEvent::Insert("abc".into()));
553 assert_eq!(input.state().value, "abc");
554
555 input.handle_text_input(InputTextEvent::Undo);
556 assert_eq!(input.state().value, "");
557
558 input.handle_text_input(InputTextEvent::Redo);
559 assert_eq!(input.state().value, "abc");
560 }
561
562 #[test]
563 fn paste_inserts_at_caret() {
564 let input = make_component("ab", "");
565 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
566 input.handle_text_input(InputTextEvent::Paste("XY".into()));
567 assert_eq!(input.state().value, "aXYb");
568 }
569
570 #[test]
571 fn enter_keeps_value_and_clears_preedit() {
572 let input = make_component("abc", "");
573 input.handle_text_input(InputTextEvent::Preedit("にほ".into()));
574 input.handle_text_input(InputTextEvent::Enter);
575 let state = input.state();
576 assert_eq!(state.value, "abc");
577 assert!(state.preedit.is_empty());
578 }
579
580 #[test]
581 fn role_and_value_for_accessibility() {
582 let input = make_component("hello", "Name");
583 assert_eq!(input.role(), Some("textbox"));
584 assert_eq!(input.label(), Some("Name".to_string()));
585 assert_eq!(input.value(), Some("hello".to_string()));
586 }
587
588 #[test]
589 fn on_value_change_callback_fires() {
590 use std::sync::{Arc, Mutex};
591 let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
592 let received_clone = Arc::clone(&received);
593 let cb: Arc<OnValueChange> = Arc::new(move |v: &str| {
594 received_clone.lock().unwrap().push(v.to_string());
595 });
596 let input = InputTextComponent::with_on_change("", "", Arc::new(FallbackTextMeasurer), cb);
597
598 input.handle_text_input(InputTextEvent::Insert("hello".into()));
599 assert_eq!(*received.lock().unwrap(), vec!["hello"]);
600
601 input.handle_text_input(InputTextEvent::Insert(" world".into()));
602 assert_eq!(*received.lock().unwrap(), vec!["hello", "hello world"]);
603 }
604
605 #[test]
606 fn on_enter_callback_fires_with_current_value() {
607 use std::sync::{Arc, Mutex};
608 let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
609 let received_clone = Arc::clone(&received);
610 let cb: Arc<OnValueChange> = Arc::new(move |v: &str| {
611 received_clone.lock().unwrap().push(v.to_string());
612 });
613 let input = InputTextComponent::with_on_enter("", "", Arc::new(FallbackTextMeasurer), cb);
614
615 input.handle_text_input(InputTextEvent::Insert("https://example.com".into()));
616 input.handle_text_input(InputTextEvent::Enter);
617 assert_eq!(*received.lock().unwrap(), vec!["https://example.com"]);
618 }
619
620 #[test]
621 fn set_value_updates_value_and_caret() {
622 let input = make_component("old", "");
623 input.set_value("new value");
624 let state = input.state();
625 assert_eq!(state.value, "new value");
626 assert_eq!(state.caret, "new value".len());
627 assert!(input.needs_repaint());
628 }
629}