Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Soft keyboard input for iOS #3571

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions src/platform_impl/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod app_delegate;
mod app_state;
mod event_loop;
mod monitor;
mod text_field;
mod uikit;
mod view;
mod view_controller;
Expand Down
149 changes: 149 additions & 0 deletions src/platform_impl/ios/text_field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#![allow(clippy::unnecessary_cast)]

use std::cell::RefCell;

use icrate::Foundation::{CGPoint, CGRect, CGSize, MainThreadMarker, NSObject, NSObjectProtocol};
use objc2::rc::Id;
use objc2::runtime::ProtocolObject;
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass, extern_methods};
use super::app_state::{self, EventWrapper};

use super::uikit::{UIResponder, UITextView, UITextViewDelegate};
use super::window::WinitUIWindow;
use crate::{
keyboard::{
KeyCode,
PhysicalKey,
Key,
KeyLocation,
},
dpi::PhysicalPosition,
event::{Event, KeyEvent, Force, Touch, TouchPhase, WindowEvent},
platform_impl::platform::DEVICE_ID,
window::{WindowAttributes, WindowId as RootWindowId},
};

pub struct WinitTextFieldState {
delegate: RefCell<Id<WinitTextFieldDelegate>>,
}

declare_class!(
pub(crate) struct WinitTextField;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect you don't need to subclass UITextField, i.e. this class is unnecessary as long as you make sure to store the WinitTextFieldDelegate somewhere else (e.g. on WinitView, as done currently).


unsafe impl ClassType for WinitTextField {
#[inherits(UIResponder, NSObject)]
type Super = UITextView;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "WinitUITextView";
}

impl DeclaredClass for WinitTextField {
type Ivars = WinitTextFieldState;
}

unsafe impl WinitTextField { }
);
extern_methods!(
unsafe impl WinitTextField {
// These are methods from UIResponder
#[method(becomeFirstResponder)]
pub fn focus(&self) -> bool;

#[method(resignFirstResponder)]
pub fn unfocus(&self) -> bool;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be moved to an extern_methods! on UIResponder.


fn window(&self) -> Option<Id<WinitUIWindow>> {
unsafe { msg_send_id![self, window] }
}
Comment on lines +48 to +50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have this on UIView and return UIWindow, and then when the WinitUIWindow is needed, you'd use the method on UIView, but with an Id::cast to turn it into WinitUIWindow.

}
);

declare_class!(
pub(crate) struct WinitTextFieldDelegate;

unsafe impl ClassType for WinitTextFieldDelegate {
type Super = NSObject;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "WinitTextViewDelegate";
}

impl DeclaredClass for WinitTextFieldDelegate {
type Ivars = ();
}

unsafe impl NSObjectProtocol for WinitTextFieldDelegate {}
unsafe impl UITextViewDelegate for WinitTextFieldDelegate {
#[method(textViewDidBeginEditing:)]
unsafe fn text_field_did_begin_editing(&self, sender: &WinitTextField) {
let text = sender.text();
//println!("DidBeginEditing: {text}");
}

#[method(textViewDidEndEditing:)]
unsafe fn text_field_did_end_editing(&self, sender: &WinitTextField) {
let text = sender.text();
//println!("DidEndEditing: {text}");
}

#[method(textViewDidChange:)]
unsafe fn text_field_did_change(&self, sender: &WinitTextField) {
let text = sender.text();
//println!("textViewDidChange: {text}");
sender.text_changed();
}
}
);


impl WinitTextField {

pub(crate) fn new(mtm: MainThreadMarker) -> Id<Self> {
// TODO: This should be hidden someplace.
let frame = CGRect {
origin: CGPoint { x: -20.0, y: -50.0 },
size: CGSize {
width: 200.0,
height: 40.0,
},
};
let delegate: Id<WinitTextFieldDelegate> = unsafe { objc2::msg_send_id![mtm.alloc(), init]};
let this = Self::alloc().set_ivars( WinitTextFieldState{
delegate: RefCell::new(delegate),
});
let this: Id<WinitTextField> = unsafe { msg_send_id![super(this), init] };

{
let delegate = this.ivars().delegate.borrow();
this.setDelegate(Some(ProtocolObject::from_ref(&*delegate.clone())));
}

this.setFrame(frame);

this
}
fn text_changed(&self) {
let window = self.window().unwrap();
let mtm = MainThreadMarker::new().unwrap();
let text = self.text();
let text = text.to_string();
app_state::handle_nonuser_event(
mtm,
EventWrapper::StaticEvent(Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
event: KeyEvent {
physical_key: PhysicalKey::Code(KeyCode::F35),
logical_key: Key::Character(text.clone().into()),
text: Some(text.into()),
location: KeyLocation::Standard,
state: crate::event::ElementState::Pressed,
repeat: false,
platform_specific: super::KeyEventExtra{},
},
is_synthetic: false,
},
}),
);
}
}
2 changes: 2 additions & 0 deletions src/platform_impl/ios/uikit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod responder;
mod screen;
mod screen_mode;
mod status_bar_style;
mod text_field;
mod touch;
mod trait_collection;
mod view;
Expand All @@ -34,6 +35,7 @@ pub(crate) use self::responder::UIResponder;
pub(crate) use self::screen::{UIScreen, UIScreenOverscanCompensation};
pub(crate) use self::screen_mode::UIScreenMode;
pub(crate) use self::status_bar_style::UIStatusBarStyle;
pub(crate) use self::text_field::{UITextView, UITextViewDelegate};
pub(crate) use self::touch::{UITouch, UITouchPhase, UITouchType};
pub(crate) use self::trait_collection::{UIForceTouchCapability, UITraitCollection};
#[allow(unused_imports)]
Expand Down
56 changes: 56 additions & 0 deletions src/platform_impl/ios/uikit/text_field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use super::UIView;
use icrate::Foundation::{NSObject, NSString};
use objc2::mutability::IsMainThreadOnly;
use objc2::rc::Id;
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2::{extern_class, extern_methods, extern_protocol, mutability, ClassType, ProtocolType};

extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UITextView;

unsafe impl ClassType for UITextView {
#[inherits(NSObject)]
simlay marked this conversation as resolved.
Show resolved Hide resolved
type Super = UIView;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UITextView {
#[method(text)]
pub fn text(&self) -> &NSString;
simlay marked this conversation as resolved.
Show resolved Hide resolved

#[method(setText:)]
pub fn setText(&self, text: &NSString);

#[method_id(@__retain_semantics Other delegate)]
simlay marked this conversation as resolved.
Show resolved Hide resolved
pub unsafe fn delegate(&self) -> Option<Id<ProtocolObject<dyn UITextViewDelegate>>>;

#[method(setDelegate:)]
pub fn setDelegate(&self, delegate: Option<&ProtocolObject<dyn UITextViewDelegate>>);
}
);
extern_protocol!(
pub unsafe trait UITextViewDelegate: NSObjectProtocol + IsMainThreadOnly {
#[optional]
#[method(textViewShouldBeginEditing:)]
unsafe fn textViewShouldBeginEditing(&self, sender: &UITextView) -> bool;

#[optional]
#[method(textViewDidBeginEditing:)]
unsafe fn textViewDidBeginEditing(&self, sender: &UITextView);

#[optional]
#[method(textViewShouldEndEditing:)]
unsafe fn textViewShouldEndEditing(&self, sender: &UITextView) -> bool;

#[optional]
#[method(textViewDidEndEditing:)]
unsafe fn textViewDidEndEditing(&self, sender: &UITextView);

#[optional]
#[method(textViewDidChange:)]
unsafe fn textViewDidChange(&self, sender: &UITextView);
}
unsafe impl ProtocolType for dyn UITextViewDelegate {}
);
3 changes: 3 additions & 0 deletions src/platform_impl/ios/uikit/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ extern_methods!(
#[method(setRootViewController:)]
pub fn setRootViewController(&self, rootViewController: Option<&UIViewController>);

#[method(addSubview:)]
pub fn addSubview(&self, view: &UIView);

#[method(convertRect:toCoordinateSpace:)]
pub fn convertRect_toCoordinateSpace(
&self,
Expand Down
15 changes: 13 additions & 2 deletions src/platform_impl/ios/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ use objc2::{class, declare_class, msg_send, msg_send_id, mutability, ClassType,
use tracing::{debug, warn};

use super::app_state::EventWrapper;
use super::text_field::WinitTextField;
use super::uikit::{
UIApplication, UIResponder, UIScreen, UIScreenOverscanCompensation, UIViewController, UIWindow,
};
use super::view::WinitView;
use super::view_controller::WinitViewController;

use crate::{
cursor::Cursor,
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
Expand Down Expand Up @@ -107,6 +109,7 @@ pub struct Inner {
window: Id<WinitUIWindow>,
view_controller: Id<WinitViewController>,
view: Id<WinitView>,
text_field: Id<WinitTextField>,
gl_or_metal_backed: bool,
}

Expand Down Expand Up @@ -370,8 +373,12 @@ impl Inner {
warn!("`Window::set_ime_cursor_area` is ignored on iOS")
}

pub fn set_ime_allowed(&self, _allowed: bool) {
warn!("`Window::set_ime_allowed` is ignored on iOS")
pub fn set_ime_allowed(&self, allowed: bool) {
if allowed {
self.text_field.focus();
} else {
self.text_field.unfocus();
}
}

pub fn set_ime_purpose(&self, _purpose: ImePurpose) {
Expand Down Expand Up @@ -515,6 +522,9 @@ impl Window {
};

let view = WinitView::new(mtm, &window_attributes, frame);
let text_field = WinitTextField::new(mtm);

view.addSubview(text_field.as_super());

let gl_or_metal_backed = unsafe {
let layer_class = WinitView::layerClass();
Expand Down Expand Up @@ -564,6 +574,7 @@ impl Window {
window,
view_controller,
view,
text_field,
gl_or_metal_backed,
};
Ok(Window {
Expand Down