@@ -5,7 +5,7 @@ use std::fs::File;
55use std:: io:: { BufRead , BufReader } ;
66use std:: marker:: PhantomData ;
77use std:: ops:: Deref ;
8- use std:: os:: raw;
8+ use std:: os:: raw:: { self , c_void } ;
99use std:: os:: unix:: prelude:: * ;
1010use std:: ptr:: NonNull ;
1111use std:: sync:: { Arc , RwLock } ;
@@ -28,6 +28,7 @@ use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, Wind
2828mod ffi;
2929
3030pub mod input;
31+ use crate :: input:: { TextInputState , TextSpan } ;
3132use input:: { Axis , InputEvent , KeyEvent , MotionEvent } ;
3233
3334// The only time it's safe to update the android_app->savedState pointer is
@@ -346,6 +347,122 @@ impl AndroidAppInner {
346347 }
347348 }
348349
350+ unsafe extern "C" fn map_input_state_to_text_event_callback (
351+ context : * mut c_void ,
352+ state : * const ffi:: GameTextInputState ,
353+ ) {
354+ // Java uses a modified UTF-8 format, which is a modified cesu8 format
355+ let out_ptr: * mut TextInputState = context. cast ( ) ;
356+ let text_modified_utf8: * const u8 = ( * state) . text_UTF8 . cast ( ) ;
357+ let text_modified_utf8 =
358+ std:: slice:: from_raw_parts ( text_modified_utf8, ( * state) . text_length as usize ) ;
359+ match cesu8:: from_java_cesu8 ( & text_modified_utf8) {
360+ Ok ( str) => {
361+ ( * out_ptr) . text = String :: from ( str) ;
362+ ( * out_ptr) . selection = TextSpan {
363+ start : match ( * state) . selection . start {
364+ -1 => None ,
365+ off => Some ( off as usize ) ,
366+ } ,
367+ end : match ( * state) . selection . end {
368+ -1 => None ,
369+ off => Some ( off as usize ) ,
370+ } ,
371+ } ;
372+ ( * out_ptr) . compose_region = TextSpan {
373+ start : match ( * state) . composingRegion . start {
374+ -1 => None ,
375+ off => Some ( off as usize ) ,
376+ } ,
377+ end : match ( * state) . composingRegion . end {
378+ -1 => None ,
379+ off => Some ( off as usize ) ,
380+ } ,
381+ } ;
382+ }
383+ Err ( err) => {
384+ log:: error!( "Invalid UTF8 text in TextEvent: {}" , err) ;
385+ }
386+ }
387+ }
388+
389+ // TODO: move into a trait
390+ pub fn text_input_state ( & self ) -> TextInputState {
391+ unsafe {
392+ let activity = ( * self . native_app . as_ptr ( ) ) . activity ;
393+ let mut out_state = TextInputState {
394+ text : String :: new ( ) ,
395+ selection : TextSpan {
396+ start : None ,
397+ end : None ,
398+ } ,
399+ compose_region : TextSpan {
400+ start : None ,
401+ end : None ,
402+ } ,
403+ } ;
404+ let out_ptr = & mut out_state as * mut TextInputState ;
405+
406+ // NEON WARNING:
407+ //
408+ // It's not clearly documented but the GameActivity API over the
409+ // GameTextInput library directly exposes _modified_ UTF8 text
410+ // from Java so we need to be careful to convert text to and
411+ // from UTF8
412+ //
413+ // GameTextInput also uses a pre-allocated, fixed-sized buffer for the current
414+ // text state but GameTextInput doesn't actually provide it's own thread
415+ // safe API to safely access this state so we have to cooperate with
416+ // the GameActivity code that does locking when reading/writing the state
417+ // (I.e. we can't just punch through to the GameTextInput layer from here).
418+ //
419+ // Overall this is all quite gnarly - and probably a good reminder of why
420+ // we want to use Rust instead of C/C++.
421+ ffi:: GameActivity_getTextInputState (
422+ activity,
423+ Some ( AndroidAppInner :: map_input_state_to_text_event_callback) ,
424+ out_ptr. cast ( ) ,
425+ ) ;
426+
427+ out_state
428+ }
429+ }
430+
431+ // TODO: move into a trait
432+ pub fn set_text_input_state ( & self , state : TextInputState ) {
433+ unsafe {
434+ let activity = ( * self . native_app . as_ptr ( ) ) . activity ;
435+ let modified_utf8 = cesu8:: to_java_cesu8 ( & state. text ) ;
436+ let text_length = modified_utf8. len ( ) as i32 ;
437+ let modified_utf8_bytes = modified_utf8. as_ptr ( ) ;
438+ let ffi_state = ffi:: GameTextInputState {
439+ text_UTF8 : modified_utf8_bytes. cast ( ) , // NB: may be signed or unsigned depending on target
440+ text_length,
441+ selection : ffi:: GameTextInputSpan {
442+ start : match state. selection . start {
443+ Some ( off) => off as i32 ,
444+ None => -1 ,
445+ } ,
446+ end : match state. selection . end {
447+ Some ( off) => off as i32 ,
448+ None => -1 ,
449+ } ,
450+ } ,
451+ composingRegion : ffi:: GameTextInputSpan {
452+ start : match state. compose_region . start {
453+ Some ( off) => off as i32 ,
454+ None => -1 ,
455+ } ,
456+ end : match state. compose_region . end {
457+ Some ( off) => off as i32 ,
458+ None => -1 ,
459+ } ,
460+ } ,
461+ } ;
462+ ffi:: GameActivity_setTextInputState ( activity, & ffi_state as * const _ ) ;
463+ }
464+ }
465+
349466 pub fn enable_motion_axis ( & mut self , axis : Axis ) {
350467 unsafe { ffi:: GameActivityPointerAxes_enableAxis ( axis as i32 ) }
351468 }
@@ -408,6 +525,15 @@ impl AndroidAppInner {
408525 for motion_event in buf. motion_events_iter ( ) {
409526 callback ( & InputEvent :: MotionEvent ( motion_event) ) ;
410527 }
528+
529+ unsafe {
530+ let app_ptr = self . native_app . as_ptr ( ) ;
531+ if ( * app_ptr) . textInputState != 0 {
532+ let state = self . text_input_state ( ) ;
533+ callback ( & InputEvent :: TextEvent ( state) ) ;
534+ ( * app_ptr) . textInputState = 0 ;
535+ }
536+ }
411537 }
412538
413539 pub fn internal_data_path ( & self ) -> Option < std:: path:: PathBuf > {
0 commit comments