@@ -32,6 +32,7 @@ use crate::{
32
32
mod ffi;
33
33
34
34
pub mod input;
35
+ use crate :: input:: { TextInputState , TextSpan } ;
35
36
use input:: { Axis , InputEvent , KeyEvent , MotionEvent } ;
36
37
37
38
// The only time it's safe to update the android_app->savedState pointer is
@@ -360,6 +361,121 @@ impl AndroidAppInner {
360
361
}
361
362
}
362
363
364
+ unsafe extern "C" fn map_input_state_to_text_event_callback (
365
+ context : * mut c_void ,
366
+ state : * const ffi:: GameTextInputState ,
367
+ ) {
368
+ // Java uses a modified UTF-8 format, which is a modified cesu8 format
369
+ let out_ptr: * mut TextInputState = context. cast ( ) ;
370
+ let text_modified_utf8: * const u8 = ( * state) . text_UTF8 . cast ( ) ;
371
+ let text_modified_utf8 =
372
+ std:: slice:: from_raw_parts ( text_modified_utf8, ( * state) . text_length as usize ) ;
373
+ match cesu8:: from_java_cesu8 ( & text_modified_utf8) {
374
+ Ok ( str) => {
375
+ let len = * & str. len ( ) ;
376
+ ( * out_ptr) . text = String :: from ( str) ;
377
+
378
+ let selection_start = ( * state) . selection . start . clamp ( 0 , len as i32 + 1 ) ;
379
+ let selection_end = ( * state) . selection . end . clamp ( 0 , len as i32 + 1 ) ;
380
+ ( * out_ptr) . selection = TextSpan {
381
+ start : selection_start as usize ,
382
+ end : selection_end as usize ,
383
+ } ;
384
+ if ( * state) . composingRegion . start < 0 || ( * state) . composingRegion . end < 0 {
385
+ ( * out_ptr) . compose_region = None ;
386
+ } else {
387
+ ( * out_ptr) . compose_region = Some ( TextSpan {
388
+ start : ( * state) . composingRegion . start as usize ,
389
+ end : ( * state) . composingRegion . end as usize ,
390
+ } ) ;
391
+ }
392
+ }
393
+ Err ( err) => {
394
+ log:: error!( "Invalid UTF8 text in TextEvent: {}" , err) ;
395
+ }
396
+ }
397
+ }
398
+
399
+ // TODO: move into a trait
400
+ pub fn text_input_state ( & self ) -> TextInputState {
401
+ unsafe {
402
+ let activity = ( * self . native_app . as_ptr ( ) ) . activity ;
403
+ let mut out_state = TextInputState {
404
+ text : String :: new ( ) ,
405
+ selection : TextSpan { start : 0 , end : 0 } ,
406
+ compose_region : None ,
407
+ } ;
408
+ let out_ptr = & mut out_state as * mut TextInputState ;
409
+
410
+ // NEON WARNING:
411
+ //
412
+ // It's not clearly documented but the GameActivity API over the
413
+ // GameTextInput library directly exposes _modified_ UTF8 text
414
+ // from Java so we need to be careful to convert text to and
415
+ // from UTF8
416
+ //
417
+ // GameTextInput also uses a pre-allocated, fixed-sized buffer for the current
418
+ // text state but GameTextInput doesn't actually provide it's own thread
419
+ // safe API to safely access this state so we have to cooperate with
420
+ // the GameActivity code that does locking when reading/writing the state
421
+ // (I.e. we can't just punch through to the GameTextInput layer from here).
422
+ //
423
+ // Overall this is all quite gnarly - and probably a good reminder of why
424
+ // we want to use Rust instead of C/C++.
425
+ ffi:: GameActivity_getTextInputState (
426
+ activity,
427
+ Some ( AndroidAppInner :: map_input_state_to_text_event_callback) ,
428
+ out_ptr. cast ( ) ,
429
+ ) ;
430
+
431
+ out_state
432
+ }
433
+ }
434
+
435
+ // TODO: move into a trait
436
+ pub fn set_text_input_state ( & self , state : TextInputState ) {
437
+ unsafe {
438
+ let activity = ( * self . native_app . as_ptr ( ) ) . activity ;
439
+ let modified_utf8 = cesu8:: to_java_cesu8 ( & state. text ) ;
440
+ let text_length = modified_utf8. len ( ) as i32 ;
441
+ let modified_utf8_bytes = modified_utf8. as_ptr ( ) ;
442
+ let ffi_state = ffi:: GameTextInputState {
443
+ text_UTF8 : modified_utf8_bytes. cast ( ) , // NB: may be signed or unsigned depending on target
444
+ text_length,
445
+ selection : ffi:: GameTextInputSpan {
446
+ start : state. selection . start as i32 ,
447
+ end : state. selection . end as i32 ,
448
+ } ,
449
+ composingRegion : match state. compose_region {
450
+ Some ( span) => {
451
+ // The GameText subclass of InputConnection only has a special case for removing the
452
+ // compose region if `start == -1` but the docs for `setComposingRegion` imply that
453
+ // the region should effectively be removed if any empty region is given (unlike for the
454
+ // selection region, it's not meaningful to maintain an empty compose region)
455
+ //
456
+ // We aim for more consistent behaviour by normalizing any empty region into `(-1, -1)`
457
+ // to remove the compose region.
458
+ //
459
+ // NB `setComposingRegion` itself is documented to clamp start/end to the text bounds
460
+ // so apart from this special-case handling in GameText's implementation of
461
+ // `setComposingRegion` then there's nothing special about `(-1, -1)` - it's just an empty
462
+ // region that should get clamped to `(0, 0)` and then get removed.
463
+ if span. start == span. end {
464
+ ffi:: GameTextInputSpan { start : -1 , end : -1 }
465
+ } else {
466
+ ffi:: GameTextInputSpan {
467
+ start : span. start as i32 ,
468
+ end : span. end as i32 ,
469
+ }
470
+ }
471
+ }
472
+ None => ffi:: GameTextInputSpan { start : -1 , end : -1 } ,
473
+ } ,
474
+ } ;
475
+ ffi:: GameActivity_setTextInputState ( activity, & ffi_state as * const _ ) ;
476
+ }
477
+ }
478
+
363
479
pub fn enable_motion_axis ( & mut self , axis : Axis ) {
364
480
unsafe { ffi:: GameActivityPointerAxes_enableAxis ( axis as i32 ) }
365
481
}
@@ -403,7 +519,7 @@ impl AndroidAppInner {
403
519
}
404
520
}
405
521
406
- pub fn input_events < F > ( & self , mut callback : F )
522
+ fn dispatch_key_and_motion_events < F > ( & self , mut callback : F )
407
523
where
408
524
F : FnMut ( & InputEvent ) -> InputStatus ,
409
525
{
@@ -426,6 +542,28 @@ impl AndroidAppInner {
426
542
}
427
543
}
428
544
545
+ fn dispatch_text_events < F > ( & self , mut callback : F )
546
+ where
547
+ F : FnMut ( & InputEvent ) -> InputStatus ,
548
+ {
549
+ unsafe {
550
+ let app_ptr = self . native_app . as_ptr ( ) ;
551
+ if ( * app_ptr) . textInputState != 0 {
552
+ let state = self . text_input_state ( ) ;
553
+ callback ( & InputEvent :: TextEvent ( state) ) ;
554
+ ( * app_ptr) . textInputState = 0 ;
555
+ }
556
+ }
557
+ }
558
+
559
+ pub fn input_events < F > ( & self , mut callback : F )
560
+ where
561
+ F : FnMut ( & InputEvent ) -> InputStatus ,
562
+ {
563
+ self . dispatch_key_and_motion_events ( & mut callback) ;
564
+ self . dispatch_text_events ( & mut callback) ;
565
+ }
566
+
429
567
pub fn internal_data_path ( & self ) -> Option < std:: path:: PathBuf > {
430
568
unsafe {
431
569
let app_ptr = self . native_app . as_ptr ( ) ;
0 commit comments