#![allow(non_snake_case)] use std::{cell::RefCell, rc::Rc}; use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; use objc2_foundation::{CGSize, NSMutableAttributedString, NSObject, NSObjectProtocol, NSSet, NSString, NSUInteger}; use objc2_ui_kit::{UIEvent, UIKeyInput, UIResponder, UITextInput, UITextInputTraits, UITouch, UITouchPhase, UIView}; use objc2::{rc::Id, runtime::AnyObject, runtime::ProtocolObject}; use objc2_foundation::{ CGPoint, CGRect, MainThreadMarker, NSArray, NSAttributedStringKey, NSComparisonResult, NSDictionary, NSInteger, NSRange, }; use objc2_ui_kit::{ NSWritingDirection, UIPress, UIPressesEvent, UITextInputDelegate, UITextInputStringTokenizer, UITextInputTokenizer, UITextLayoutDirection, UITextPosition, UITextRange, UITextSelectionRect, UITextStorageDirection, }; use crate::MTKView::MTKView; use super::{Vars, WinKey, WinRef, WinTouch}; pub struct ViewVars { vars: Rc, input_delegate: RefCell>>>, tokenizer: RefCell>>, maked: RefCell>, } impl ViewVars { pub fn new(vars: Rc) -> Self { Self { vars, input_delegate: Default::default(), tokenizer: Default::default(), maked: RefCell::new(NSMutableAttributedString::new()), } } } declare_class! { pub struct XLoopView; unsafe impl ClassType for XLoopView { #[inherits(UIView,UIResponder,NSObject)] type Super = MTKView; type Mutability = mutability::MainThreadOnly; const NAME: &'static str = "XLoopView"; } impl DeclaredClass for XLoopView { type Ivars = ViewVars; } unsafe impl NSObjectProtocol for XLoopView {} unsafe impl UITextInputTraits for XLoopView {} unsafe impl UIKeyInput for XLoopView { #[method(hasText)] unsafe fn hasText(&self) -> bool { // log::info!("hasText"); self.ivars().maked.borrow().length() > 0 // false } #[method(insertText:)] unsafe fn insertText(&self, text: &NSString) { log::warn!("insertText:{text}"); let string = text.to_string(); self.commit(types::EvtCommit::new(types::Action::End,Some(string.into()))); // self.ivars().text_store.borrow_mut().appendAttributedString(&NSMutableAttributedString::from_nsstring(text)); } #[method(deleteBackward)] unsafe fn deleteBackward(&self){ log::warn!("deleteBackward"); self.commit(types::EvtCommit::new(types::Action::Back,None)); // let length = self.ivars().text_store.borrow().length(); // self.ivars().text_store.borrow_mut().deleteCharactersInRange(NSRange::new(length-1,1)) } } unsafe impl UITextInput for XLoopView { // Handling text input #[method_id(inputDelegate)] unsafe fn inputDelegate(&self)-> Option>>{ log::info!("inputDelegate"); if let Some(v) = self.ivars().input_delegate.borrow().as_ref(){ Id::retain(Id::as_ptr(v) as *mut _) }else{ None } } #[method(setInputDelegate:)] unsafe fn setInputDelegate(&self,v: Option<&ProtocolObject>){ log::info!("setInputDelegate"); if let Some(v) = v { let v:Option>> = Id::retain(v as *const _ as *mut _); *self.ivars().input_delegate.borrow_mut() = v; } } // Replacing and returning text #[method_id(textInRange:)] unsafe fn textInRange(&self, range: &UITextRange) -> Option>{ let range = TextRange::from_ui(range).to_range(); let ret = self.ivars().maked.borrow().mutableString().substringWithRange(range); // log::info!("textInRange {}-{} ==={ret}===",range.location,range.length); Some(ret) } #[method(replaceRange:withText:)] unsafe fn replaceRange_withText(&self, range: &UITextRange, string: &NSString){ log::warn!("replaceRange_withText {string:?}"); let range = TextRange::from_ui(range).to_range(); self.ivars().maked.borrow_mut().replaceCharactersInRange_withString(range,string); } // Working with marked and selected text #[method_id(selectedTextRange)] unsafe fn selectedTextRange(&self) -> Option> { let length = self.ivars().maked.borrow().length(); // log::info!("selectedTextRange {}-{}",length,length); Some(TextRange::new(length,length).to_ui()) } #[method(setSelectedTextRange:)] unsafe fn setSelectedTextRange(&self, _: Option<&UITextRange>){ log::info!("setSelectedTextRange"); } #[method_id(markedTextRange)] unsafe fn markedTextRange(&self) -> Option> { let length = self.ivars().maked.borrow().length(); // log::info!("markedTextRange 0-{}",length); Some(TextRange::new(0,length).to_ui()) } #[method_id(markedTextStyle)] unsafe fn markedTextStyle(&self) -> Option>>{ log::info!("markedTextStyle"); None } #[method(setMarkedTextStyle:)] unsafe fn setMarkedTextStyle(&self,_: Option<&NSDictionary>){ log::info!("setMarkedTextStyle"); } #[method(setMarkedText:selectedRange:)] unsafe fn setMarkedText_selectedRange(&self,string: Option<&NSString>,_range: NSRange){ // log::warn!("setMarkedText_selectedRange {string:?} {}-{}",_range.location,_range.length); if let Some(string) = string { let began = self.ivars().maked.borrow().length()<=0; *self.ivars().maked.borrow_mut()=NSMutableAttributedString::from_nsstring(string); let action = began.then_some(types::Action::Update).unwrap_or(types::Action::Begin); self.commit(types::EvtCommit::new(action,Some(string.to_string().into()))); } } #[method(unmarkText)] unsafe fn unmarkText(&self){ let string = self.ivars().maked.borrow().mutableString(); self.commit(types::EvtCommit::new(types::Action::End,Some(string.to_string().into()))); log::warn!("unmarkText {}",self.ivars().maked.borrow().mutableString()); *self.ivars().maked.borrow_mut() = NSMutableAttributedString::new(); } // Computing text ranges and text positions #[method_id(textRangeFromPosition:toPosition:)] unsafe fn textRangeFromPosition_toPosition(&self, start: &UITextPosition, end: &UITextPosition) -> Option>{ // log::info!("textRangeFromPosition_toPosition"); let start = TextPosition::from_ui(start).position(); let end = TextPosition::from_ui(end).position(); Some(TextRange::new(start,end).to_ui()) } #[method_id(positionFromPosition:inDirection:offset:)] unsafe fn positionFromPosition_inDirection_offset(&self,_: &UITextPosition,_: UITextLayoutDirection,_: NSInteger) -> Option>{ log::info!("positionFromPosition_inDirection_offset"); None } #[method_id(positionFromPosition:offset:)] unsafe fn positionFromPosition_offset(&self,start: &UITextPosition,shift: NSInteger) -> Option>{ let start = TextPosition::from_ui(start).position() as NSInteger; let end = start + shift; // log::info!("positionFromPosition_offset {} {}",start,shift); if end >= 0 { let end = end as NSUInteger; let length = self.ivars().maked.borrow().length(); if end < length { Some(TextPosition::new(end as _).to_ui()) }else{ Some(TextPosition::new(length).to_ui()) } }else{ Some(TextPosition::new(0).to_ui()) } } #[method_id(beginningOfDocument)] unsafe fn beginningOfDocument(&self) -> Id{ // log::info!("beginningOfDocument"); TextPosition::new(0).to_ui() } #[method_id(endOfDocument)] unsafe fn endOfDocument(&self) -> Id{ // log::info!("endOfDocument"); let length = self.ivars().maked.borrow().length(); TextPosition::new(length).to_ui() } // Evaluating text positions #[method(comparePosition:toPosition:)] unsafe fn comparePosition_toPosition(&self,v1: &UITextPosition,v2: &UITextPosition) -> NSComparisonResult{ // log::info!("comparePosition_toPosition"); let v1 = TextPosition::from_ui(v1).position(); let v2 = TextPosition::from_ui(v2).position(); if v1 > v2 { NSComparisonResult::Descending }else if v1 < v2 { NSComparisonResult::Ascending }else { NSComparisonResult::Same } } #[method(offsetFromPosition:toPosition:)] unsafe fn offsetFromPosition_toPosition(&self,v1: &UITextPosition,v2: &UITextPosition,) -> NSInteger{ let start = TextPosition::from_ui(v1).position(); let end = TextPosition::from_ui(v2).position(); // log::info!("offsetFromPosition_toPosition {} {}",start,end); (end - start) as _ } // Determining layout and writing direction #[method_id(positionWithinRange:farthestInDirection:)] unsafe fn positionWithinRange_farthestInDirection(&self,_: &UITextRange,_: UITextLayoutDirection) -> Option>{ log::info!("positionWithinRange_farthestInDirection"); None } #[method_id(characterRangeByExtendingPosition:inDirection:)] unsafe fn characterRangeByExtendingPosition_inDirection(&self,_: &UITextPosition,_: UITextLayoutDirection) -> Option>{ log::info!("characterRangeByExtendingPosition_inDirection"); None } #[method(baseWritingDirectionForPosition:inDirection:)] unsafe fn baseWritingDirectionForPosition_inDirection(&self,_: &UITextPosition,_: UITextStorageDirection) -> NSWritingDirection{ log::info!("baseWritingDirectionForPosition_inDirection"); NSWritingDirection::Natural } #[method(setBaseWritingDirection:forRange:)] unsafe fn setBaseWritingDirection_forRange(&self,_: NSWritingDirection,_: &UITextRange){ log::info!("setBaseWritingDirection_forRange"); } // Working with geometry and hit-testing #[method(firstRectForRange:)] unsafe fn firstRectForRange(&self, range: &UITextRange) -> CGRect{ let range = TextRange::from_ui(range).to_range(); // log::info!("firstRectForRange {}-{}",range.location,range.length); CGRect::new(CGPoint::new(range.location as _,0.),CGSize::new(range.length as _,1.)) } #[method(caretRectForPosition:)] unsafe fn caretRectForPosition(&self, pos: &UITextPosition) -> CGRect { let pos = TextPosition::from_ui(pos).position(); // log::info!("caretRectForPosition"); CGRect::new(CGPoint::new(pos as _,0.),CGSize::new(1.,1.)) } #[method_id(closestPositionToPoint:)] unsafe fn closestPositionToPoint(&self, _: CGPoint)-> Option>{ log::info!("closestPositionToPoint"); None } #[method_id(selectionRectsForRange:)] unsafe fn selectionRectsForRange(&self, _: &UITextRange) -> Id> { // log::info!("selectionRectsForRange"); NSArray::new() } #[method_id(closestPositionToPoint:withinRange:)] unsafe fn closestPositionToPoint_withinRange(&self,_: CGPoint,_: &UITextRange) -> Option>{ log::info!("closestPositionToPoint_withinRange"); None } #[method_id(characterRangeAtPoint:)] unsafe fn characterRangeAtPoint(&self, _: CGPoint) -> Option> { log::info!("characterRangeAtPoint"); None } // Tokenizing input text #[method_id(tokenizer)] unsafe fn tokenizer(&self) -> Id>{ // log::info!("tokenizer"); let mut tokenizer = self.ivars().tokenizer.borrow_mut(); if let Some(tokenizer) = tokenizer.as_ref() { ProtocolObject::from_retained(tokenizer.clone()) }else{ let mtm = MainThreadMarker::new().unwrap(); let new = UITextInputStringTokenizer::initWithTextInput(mtm.alloc(),self); *tokenizer = Some(new.clone()); ProtocolObject::from_retained(new) } } } unsafe impl XLoopView { #[method(draw)] unsafe fn draw(&self){ self.draw_() } #[method(canBecomeFirstResponder)] unsafe fn canBecomeFirstResponder(&self) -> bool { true } #[method(canResignFirstResponder)] unsafe fn canResignFirstResponder(&self) -> bool { true } #[method(touchesBegan:withEvent:)] fn touchesBegan(&self, touches: &NSSet, event: Option<&UIEvent>) { unsafe { if !self.isFirstResponder() { self.becomeFirstResponder() }else{ self.resignFirstResponder() }; } self.touch(touches,event) } #[method(touchesMoved:withEvent:)] fn touchesMoved(&self, touches: &NSSet, event: Option<&UIEvent>) { self.touch(touches,event) } #[method(touchesEnded:withEvent:)] fn touchesEnded(&self, touches: &NSSet, event: Option<&UIEvent>) { self.touch(touches,event) } #[method(touchesCancelled:withEvent:)] fn touchesCancelled(&self, touches: &NSSet, event: Option<&UIEvent>) { self.touch(touches,event) } #[method(pressesBegan:withEvent:)] unsafe fn pressesBegan_withEvent(&self,presses: &NSSet,event: Option<&UIPressesEvent>){ self.press(presses,event) } #[method(pressesChanged:withEvent:)] unsafe fn pressesChanged_withEvent(&self,presses: &NSSet,event: Option<&UIPressesEvent>){ self.press(presses,event) } #[method(pressesEnded:withEvent:)] unsafe fn pressesEnded_withEvent(&self,presses: &NSSet,event: Option<&UIPressesEvent>){ self.press(presses,event) } #[method(pressesCancelled:withEvent:)] unsafe fn pressesCancelled_withEvent(&self,presses: &NSSet,event: Option<&UIPressesEvent>){ self.press(presses,event) } } } impl XLoopView { fn draw_(&self) { if let Some(window) = self.ivars().vars.window.borrow().clone() { self.ivars().vars.delegate.req_draw(WinRef(&window, &self), 0); //TOOD } } fn touch(&self, touches: &NSSet, _event: Option<&UIEvent>) { let vars = self.ivars(); let mut caches = vars.vars.touches.borrow_mut(); let caches = caches.as_mut(); let mut ids = TouchId(caches); for touch in touches { let phase = touch.phase(); match phase { UITouchPhase::Began | UITouchPhase::Moved => { let id = ids.insert(touch as _); if let Some(window) = vars.vars.window.borrow().clone() { vars.vars.delegate.on_touch(WinRef(&window, &self), WinTouch(id, touch)); } } UITouchPhase::Ended | UITouchPhase::Cancelled => { let id = ids.insert(touch as _); if let Some(window) = vars.vars.window.borrow().clone() { vars.vars.delegate.on_touch(WinRef(&window, &self), WinTouch(id, touch)); } ids.remove(touch as _); } _ => { println!("UITouchPhase {:?}", phase); } } } } fn press(&self, presses: &NSSet, _event: Option<&UIPressesEvent>) { let vars = self.ivars(); for press in presses { if let Some(key) = unsafe { press.key(MainThreadMarker::new().unwrap()) } { if let Some(win) = vars.vars.window.borrow().as_ref() { vars.vars.delegate.on_key(WinRef(&win, &self), WinKey(&key, press)); } } else { // TODO other key } } } fn commit(&self, evt: types::EvtCommit) { let vars = self.ivars(); if let Some(win) = vars.vars.window.borrow().as_ref() { vars.vars.delegate.on_commit(WinRef(&win, self), &evt); } } } pub struct TouchId<'a>(pub &'a mut Vec>); impl<'a> TouchId<'a> { pub fn insert(&mut self, id: *const UITouch) -> u8 { let mut idx = self.0.iter().enumerate().find(|(_, v)| **v == Some(id)).map(|v| v.0); if idx.is_none() { idx = self.0.iter_mut().enumerate().find(|(_, v)| v.is_none()).map(|(idx, v)| { *v = Some(id); idx }) } if let Some(idx) = idx { idx as u8 } else { let idx = self.0.len(); self.0.push(Some(id)); idx as u8 } } pub fn remove(&mut self, id: *const UITouch) { self.0.iter_mut().find(|v| **v == Some(id)).map(|v| *v = None); } } declare_class! { pub struct TextPosition; unsafe impl ClassType for TextPosition { #[inherits(NSObject)] type Super = UITextPosition; type Mutability = mutability::MainThreadOnly; const NAME: &'static str = "XLoopTextPosition"; } impl DeclaredClass for TextPosition { type Ivars = NSUInteger; } } impl TextPosition { pub fn new(position: NSUInteger) -> Id { let mtm = MainThreadMarker::new().unwrap(); unsafe { msg_send_id![super(mtm.alloc().set_ivars(position)), init] } } pub fn position(&self) -> NSUInteger { *self.ivars() } pub fn to_ui(&self) -> Id { unsafe { Id::retain(self as *const _ as *mut _) }.unwrap() } pub fn from_ui(v: &UITextPosition) -> &Self { unsafe { &*(v as *const _ as *const Self) } } } declare_class! { pub struct TextRange; unsafe impl ClassType for TextRange { #[inherits(NSObject)] type Super = UITextRange; type Mutability = mutability::MainThreadOnly; const NAME: &'static str = "XLoopTextRange"; } impl DeclaredClass for TextRange { type Ivars = (Id,Id); } unsafe impl TextRange { #[method(isEmpty)] unsafe fn isEmpty(&self) -> bool{ let vars = self.ivars(); vars.1.position() > vars.0.position() } #[method_id(start)] unsafe fn start(&self) -> Id{ self.ivars().0.to_ui() } #[method_id(end)] unsafe fn end(&self) -> Id{ self.ivars().1.to_ui() } } } impl TextRange { pub fn new(start: NSUInteger, end: NSUInteger) -> Id { let mtm = MainThreadMarker::new().unwrap(); unsafe { msg_send_id![ super(mtm.alloc().set_ivars((TextPosition::new(start), TextPosition::new(end)))), init ] } } pub fn clone(&self) -> Id { Self::new(self.ivars().0.position(), self.ivars().1.position()) } pub fn length(&self) -> NSUInteger { // log::info!("length {} {}", self.ivars().1.position(), self.ivars().0.position()); self.ivars().1.position() - self.ivars().0.position() } pub fn to_range(&self) -> NSRange { NSRange::new(self.ivars().0.position(), self.length()) } pub fn to_ui(&self) -> Id { unsafe { Id::retain(self as *const _ as *mut _) }.unwrap() } pub fn from_ui(v: &UITextRange) -> &Self { unsafe { &*(v as *const _ as *const Self) } } }