diff options
Diffstat (limited to 'src/ios.m')
-rw-r--r-- | src/ios.m | 319 |
1 files changed, 318 insertions, 1 deletions
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
25 | #include "audio/player.h" | 25 | #include "audio/player.h" |
26 | #include "ui/command.h" | 26 | #include "ui/command.h" |
27 | #include "ui/window.h" | 27 | #include "ui/window.h" |
28 | #include "ui/touch.h" | ||
28 | 29 | ||
29 | #include <the_Foundation/file.h> | 30 | #include <the_Foundation/file.h> |
30 | #include <the_Foundation/fileinfo.h> | 31 | #include <the_Foundation/fileinfo.h> |
@@ -60,6 +61,9 @@ static UIViewController *viewController_(iWindow *window) { | |||
60 | return NULL; | 61 | return NULL; |
61 | } | 62 | } |
62 | 63 | ||
64 | static void notifyChange_SystemTextInput_(iSystemTextInput *); | ||
65 | static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *); | ||
66 | |||
63 | /*----------------------------------------------------------------------------------------------*/ | 67 | /*----------------------------------------------------------------------------------------------*/ |
64 | 68 | ||
65 | API_AVAILABLE(ios(13.0)) | 69 | API_AVAILABLE(ios(13.0)) |
@@ -159,9 +163,10 @@ API_AVAILABLE(ios(13.0)) | |||
159 | 163 | ||
160 | /*----------------------------------------------------------------------------------------------*/ | 164 | /*----------------------------------------------------------------------------------------------*/ |
161 | 165 | ||
162 | @interface AppState : NSObject<UIDocumentPickerDelegate> { | 166 | @interface AppState : NSObject<UIDocumentPickerDelegate, UITextFieldDelegate, UITextViewDelegate> { |
163 | iString *fileBeingSaved; | 167 | iString *fileBeingSaved; |
164 | iString *pickFileCommand; | 168 | iString *pickFileCommand; |
169 | iSystemTextInput *sysCtrl; | ||
165 | } | 170 | } |
166 | @property (nonatomic, assign) BOOL isHapticsAvailable; | 171 | @property (nonatomic, assign) BOOL isHapticsAvailable; |
167 | @property (nonatomic, strong) NSObject *haptic; | 172 | @property (nonatomic, strong) NSObject *haptic; |
@@ -175,9 +180,18 @@ static AppState *appState_; | |||
175 | self = [super init]; | 180 | self = [super init]; |
176 | fileBeingSaved = NULL; | 181 | fileBeingSaved = NULL; |
177 | pickFileCommand = NULL; | 182 | pickFileCommand = NULL; |
183 | sysCtrl = NULL; | ||
178 | return self; | 184 | return self; |
179 | } | 185 | } |
180 | 186 | ||
187 | -(void)setSystemTextInput:(iSystemTextInput *)sys { | ||
188 | sysCtrl = sys; | ||
189 | } | ||
190 | |||
191 | -(iSystemTextInput *)systemTextInput { | ||
192 | return sysCtrl; | ||
193 | } | ||
194 | |||
181 | -(void)setPickFileCommand:(const char *)command { | 195 | -(void)setPickFileCommand:(const char *)command { |
182 | if (!pickFileCommand) { | 196 | if (!pickFileCommand) { |
183 | pickFileCommand = new_String(); | 197 | pickFileCommand = new_String(); |
@@ -256,6 +270,46 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { | |||
256 | -(void)keyboardOffScreen:(NSNotification *)notification { | 270 | -(void)keyboardOffScreen:(NSNotification *)notification { |
257 | setKeyboardHeight_MainWindow(get_MainWindow(), 0); | 271 | setKeyboardHeight_MainWindow(get_MainWindow(), 0); |
258 | } | 272 | } |
273 | |||
274 | static void sendReturnKeyPress_(void) { | ||
275 | SDL_Event ev = { .type = SDL_KEYDOWN }; | ||
276 | ev.key.timestamp = SDL_GetTicks(); | ||
277 | ev.key.keysym.sym = SDLK_RETURN; | ||
278 | ev.key.state = SDL_PRESSED; | ||
279 | SDL_PushEvent(&ev); | ||
280 | ev.type = SDL_KEYUP; | ||
281 | ev.key.state = SDL_RELEASED; | ||
282 | SDL_PushEvent(&ev); | ||
283 | } | ||
284 | |||
285 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { | ||
286 | sendReturnKeyPress_(); | ||
287 | return NO; | ||
288 | } | ||
289 | |||
290 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range | ||
291 | replacementString:(NSString *)string { | ||
292 | iSystemTextInput *sysCtrl = [appState_ systemTextInput]; | ||
293 | notifyChange_SystemTextInput_(sysCtrl); | ||
294 | return YES; | ||
295 | } | ||
296 | |||
297 | - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range | ||
298 | replacementText:(NSString *)text { | ||
299 | if ([text isEqualToString:@"\n"]) { | ||
300 | if (!isNewlineAllowed_SystemTextInput_([appState_ systemTextInput])) { | ||
301 | sendReturnKeyPress_(); | ||
302 | return NO; | ||
303 | } | ||
304 | } | ||
305 | return YES; | ||
306 | } | ||
307 | |||
308 | - (void)textViewDidChange:(UITextView *)textView { | ||
309 | iSystemTextInput *sysCtrl = [appState_ systemTextInput]; | ||
310 | notifyChange_SystemTextInput_(sysCtrl); | ||
311 | } | ||
312 | |||
259 | @end | 313 | @end |
260 | 314 | ||
261 | static void enableMouse_(iBool yes) { | 315 | static void enableMouse_(iBool yes) { |
@@ -483,6 +537,35 @@ void pickFile_iOS(const char *command) { | |||
483 | [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; | 537 | [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; |
484 | } | 538 | } |
485 | 539 | ||
540 | static void openActivityView_(NSArray *activityItems) { | ||
541 | UIActivityViewController *actView = | ||
542 | [[UIActivityViewController alloc] | ||
543 | initWithActivityItems:activityItems | ||
544 | applicationActivities:nil]; | ||
545 | iWindow *win = get_Window(); | ||
546 | UIViewController *viewCtl = viewController_(win); | ||
547 | UIPopoverPresentationController *popover = [actView popoverPresentationController]; | ||
548 | if (popover) { | ||
549 | [popover setSourceView:[viewCtl view]]; | ||
550 | iInt2 tapPos = latestTapPosition_Touch(); | ||
551 | tapPos.x /= win->pixelRatio; | ||
552 | tapPos.y /= win->pixelRatio; | ||
553 | [popover setSourceRect:(CGRect){{tapPos.x - 10, tapPos.y - 10}, {20, 20}}]; | ||
554 | [popover setCanOverlapSourceViewRect:YES]; | ||
555 | } | ||
556 | [viewCtl presentViewController:actView animated:YES completion:nil]; | ||
557 | } | ||
558 | |||
559 | void openTextActivityView_iOS(const iString *text) { | ||
560 | openActivityView_(@[[NSString stringWithUTF8String:cstr_String(text)]]); | ||
561 | } | ||
562 | |||
563 | void openFileActivityView_iOS(const iString *path) { | ||
564 | NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) | ||
565 | encoding:NSUTF8StringEncoding]]; | ||
566 | openActivityView_(@[url]); | ||
567 | } | ||
568 | |||
486 | /*----------------------------------------------------------------------------------------------*/ | 569 | /*----------------------------------------------------------------------------------------------*/ |
487 | 570 | ||
488 | enum iAVFAudioPlayerState { | 571 | enum iAVFAudioPlayerState { |
@@ -511,6 +594,10 @@ void init_AVFAudioPlayer(iAVFAudioPlayer *d) { | |||
511 | 594 | ||
512 | void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) { | 595 | void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) { |
513 | setInput_AVFAudioPlayer(d, NULL, NULL); | 596 | setInput_AVFAudioPlayer(d, NULL, NULL); |
597 | if (d->player) { | ||
598 | CFBridgingRelease(d->player); | ||
599 | d->player = nil; | ||
600 | } | ||
514 | } | 601 | } |
515 | 602 | ||
516 | static const char *cacheDir_ = "~/Library/Caches/Audio"; | 603 | static const char *cacheDir_ = "~/Library/Caches/Audio"; |
@@ -607,3 +694,233 @@ iBool isStarted_AVFAudioPlayer(const iAVFAudioPlayer *d) { | |||
607 | iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { | 694 | iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { |
608 | return d->state == paused_AVFAudioPlayerState; | 695 | return d->state == paused_AVFAudioPlayerState; |
609 | } | 696 | } |
697 | |||
698 | /*----------------------------------------------------------------------------------------------*/ | ||
699 | |||
700 | struct Impl_SystemTextInput { | ||
701 | int flags; | ||
702 | void *field; /* single-line text field */ | ||
703 | void *view; /* multi-line text view */ | ||
704 | void (*textChangedFunc)(iSystemTextInput *, void *); | ||
705 | void *textChangedContext; | ||
706 | }; | ||
707 | |||
708 | iDefineTypeConstructionArgs(SystemTextInput, (iRect rect, int flags), rect, flags) | ||
709 | |||
710 | #define REF_d_field (__bridge UITextField *)d->field | ||
711 | #define REF_d_view (__bridge UITextView *)d->view | ||
712 | |||
713 | static CGRect convertToCGRect_(const iRect *rect, iBool expanded) { | ||
714 | const iWindow *win = get_Window(); | ||
715 | CGRect frame; | ||
716 | // TODO: Convert coordinates properly! | ||
717 | frame.origin.x = rect->pos.x / win->pixelRatio; | ||
718 | frame.origin.y = (rect->pos.y - gap_UI + 1) / win->pixelRatio; | ||
719 | frame.size.width = rect->size.x / win->pixelRatio; | ||
720 | frame.size.height = rect->size.y / win->pixelRatio; | ||
721 | /* Some padding to account for insets. If we just zero out the insets, the insertion point | ||
722 | may be clipped at the edges. */ | ||
723 | if (expanded) { | ||
724 | const float inset = gap_UI / get_Window()->pixelRatio; | ||
725 | frame.origin.x -= inset + 1; | ||
726 | frame.origin.y -= inset + 1; | ||
727 | frame.size.width += 2 * inset + 2; | ||
728 | frame.size.height += inset + 1 + inset; | ||
729 | } | ||
730 | return frame; | ||
731 | } | ||
732 | |||
733 | static UIColor *makeUIColor_(enum iColorId colorId) { | ||
734 | iColor color = get_Color(colorId); | ||
735 | return [UIColor colorWithRed:color.r / 255.0 | ||
736 | green:color.g / 255.0 | ||
737 | blue:color.b / 255.0 | ||
738 | alpha:color.a / 255.0]; | ||
739 | } | ||
740 | |||
741 | void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { | ||
742 | d->flags = flags; | ||
743 | d->field = NULL; | ||
744 | d->view = NULL; | ||
745 | CGRect frame = convertToCGRect_(&rect, (flags & multiLine_SystemTextInputFlags) != 0); | ||
746 | if (flags & multiLine_SystemTextInputFlags) { | ||
747 | d->view = (void *) CFBridgingRetain([[UITextView alloc] initWithFrame:frame textContainer:nil]); | ||
748 | [[viewController_(get_Window()) view] addSubview:REF_d_view]; | ||
749 | } | ||
750 | else { | ||
751 | d->field = (void *) CFBridgingRetain([[UITextField alloc] initWithFrame:frame]); | ||
752 | [[viewController_(get_Window()) view] addSubview:REF_d_field]; | ||
753 | } | ||
754 | UIControl<UITextInputTraits> *traits = (UIControl<UITextInputTraits> *) (d->view ? REF_d_view : REF_d_field); | ||
755 | if (~flags & insertNewlines_SystemTextInputFlag) { | ||
756 | [traits setReturnKeyType:UIReturnKeyDone]; | ||
757 | } | ||
758 | if (flags & returnGo_SystemTextInputFlags) { | ||
759 | [traits setReturnKeyType:UIReturnKeyGo]; | ||
760 | } | ||
761 | if (flags & returnSend_SystemTextInputFlags) { | ||
762 | [traits setReturnKeyType:UIReturnKeySend]; | ||
763 | } | ||
764 | if (flags & disableAutocorrect_SystemTextInputFlag) { | ||
765 | [traits setAutocorrectionType:UITextAutocorrectionTypeNo]; | ||
766 | [traits setSpellCheckingType:UITextSpellCheckingTypeNo]; | ||
767 | } | ||
768 | if (flags & disableAutocapitalize_SystemTextInputFlag) { | ||
769 | [traits setAutocapitalizationType:UITextAutocapitalizationTypeNone]; | ||
770 | } | ||
771 | if (flags & alignRight_SystemTextInputFlag) { | ||
772 | if (d->field) { | ||
773 | [REF_d_field setTextAlignment:NSTextAlignmentRight]; | ||
774 | } | ||
775 | if (d->view) { | ||
776 | [REF_d_view setTextAlignment:NSTextAlignmentRight]; | ||
777 | } | ||
778 | } | ||
779 | UIColor *textColor = makeUIColor_(uiInputTextFocused_ColorId); | ||
780 | UIColor *backgroundColor = makeUIColor_(uiInputBackgroundFocused_ColorId); | ||
781 | UIColor *tintColor = makeUIColor_(uiInputFrameHover_ColorId); /* use the accent color */ //uiInputCursor_ColorId); | ||
782 | [appState_ setSystemTextInput:d]; | ||
783 | if (d->field) { | ||
784 | UITextField *field = REF_d_field; | ||
785 | [field setTextColor:textColor]; | ||
786 | [field setTintColor:tintColor]; | ||
787 | [field setDelegate:appState_]; | ||
788 | [field becomeFirstResponder]; | ||
789 | } | ||
790 | else { | ||
791 | UITextView *view = REF_d_view; | ||
792 | [view setBackgroundColor:[UIColor colorWithWhite:1.0f alpha:0.0f]]; | ||
793 | [view setTextColor:textColor]; | ||
794 | [view setTintColor:tintColor]; | ||
795 | if (flags & extraPadding_SystemTextInputFlag) { | ||
796 | [view setContentInset:(UIEdgeInsets){ 0, 0, 3 * gap_UI / get_Window()->pixelRatio, 0}]; | ||
797 | } | ||
798 | [view setEditable:YES]; | ||
799 | [view setDelegate:appState_]; | ||
800 | [view becomeFirstResponder]; | ||
801 | } | ||
802 | d->textChangedFunc = NULL; | ||
803 | d->textChangedContext = NULL; | ||
804 | } | ||
805 | |||
806 | void deinit_SystemTextInput(iSystemTextInput *d) { | ||
807 | [appState_ setSystemTextInput:nil]; | ||
808 | if (d->field) { | ||
809 | [REF_d_field removeFromSuperview]; | ||
810 | CFBridgingRelease(d->field); | ||
811 | d->field = nil; | ||
812 | } | ||
813 | if (d->view) { | ||
814 | [REF_d_view removeFromSuperview]; | ||
815 | CFBridgingRelease(d->view); | ||
816 | d->view = nil; | ||
817 | } | ||
818 | } | ||
819 | |||
820 | void selectAll_SystemTextInput(iSystemTextInput *d) { | ||
821 | if (d->field) { | ||
822 | [REF_d_field selectAll:nil]; | ||
823 | } | ||
824 | if (d->view) { | ||
825 | [REF_d_view selectAll:nil]; | ||
826 | } | ||
827 | } | ||
828 | |||
829 | void setText_SystemTextInput(iSystemTextInput *d, const iString *text, iBool allowUndo) { | ||
830 | NSString *str = [NSString stringWithUTF8String:cstr_String(text)]; | ||
831 | if (d->field) { | ||
832 | [REF_d_field setText:str]; | ||
833 | if (d->flags & selectAll_SystemTextInputFlags) { | ||
834 | [REF_d_field selectAll:nil]; | ||
835 | } | ||
836 | } | ||
837 | else { | ||
838 | UITextView *view = REF_d_view; | ||
839 | // if (allowUndo) { | ||
840 | // [view selectAll:nil]; | ||
841 | // if ([view shouldChangeTextInRange:[view selectedTextRange] replacementText:@""]) { | ||
842 | // [[view textStorage] beginEditing]; | ||
843 | // [[view textStorage] replaceCharactersInRange:[view selectedRange] withString:@""]; | ||
844 | // [[view textStorage] endEditing]; | ||
845 | // } | ||
846 | // } | ||
847 | // else { | ||
848 | // TODO: How to implement `allowUndo`, given that UITextView does not exist when unfocused? | ||
849 | // Maybe keep the UITextStorage (if it has the undo?)? | ||
850 | [view setText:str]; | ||
851 | // } | ||
852 | if (d->flags & selectAll_SystemTextInputFlags) { | ||
853 | [view selectAll:nil]; | ||
854 | } | ||
855 | } | ||
856 | } | ||
857 | |||
858 | int preferredHeight_SystemTextInput(const iSystemTextInput *d) { | ||
859 | if (d->view) { | ||
860 | CGRect usedRect = [[REF_d_view layoutManager] usedRectForTextContainer:[REF_d_view textContainer]]; | ||
861 | return usedRect.size.height * get_Window()->pixelRatio; | ||
862 | } | ||
863 | return 0; | ||
864 | } | ||
865 | |||
866 | void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { | ||
867 | float height = lineHeight_Text(fontId) / get_Window()->pixelRatio; | ||
868 | UIFont *font; | ||
869 | // for (NSString *name in [UIFont familyNames]) { | ||
870 | // printf("family: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); | ||
871 | // } | ||
872 | if (fontId / maxVariants_Fonts * maxVariants_Fonts == monospace_FontId) { | ||
873 | // font = [UIFont monospacedSystemFontOfSize:0.8f * height weight:UIFontWeightRegular]; | ||
874 | // for (NSString *name in [UIFont fontNamesForFamilyName:@"Iosevka Term"]) { | ||
875 | // printf("fontname: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); | ||
876 | // } | ||
877 | font = [UIFont fontWithName:@"Iosevka-Term-Extended" size:height * 0.82f]; | ||
878 | } | ||
879 | else { | ||
880 | // font = [UIFont systemFontOfSize:0.65f * height]; | ||
881 | font = [UIFont fontWithName:@"SourceSans3-Regular" size:height * 0.7f]; | ||
882 | } | ||
883 | if (d->field) { | ||
884 | [REF_d_field setFont:font]; | ||
885 | } | ||
886 | if (d->view) { | ||
887 | [REF_d_view setFont:font]; | ||
888 | } | ||
889 | } | ||
890 | |||
891 | const iString *text_SystemTextInput(const iSystemTextInput *d) { | ||
892 | if (d->field) { | ||
893 | return collectNewCStr_String([[REF_d_field text] cStringUsingEncoding:NSUTF8StringEncoding]); | ||
894 | } | ||
895 | if (d->view) { | ||
896 | return collectNewCStr_String([[REF_d_view text] cStringUsingEncoding:NSUTF8StringEncoding]); | ||
897 | } | ||
898 | return NULL; | ||
899 | } | ||
900 | |||
901 | void setRect_SystemTextInput(iSystemTextInput *d, iRect rect) { | ||
902 | CGRect frame = convertToCGRect_(&rect, (d->flags & multiLine_SystemTextInputFlags) != 0); | ||
903 | if (d->field) { | ||
904 | [REF_d_field setFrame:frame]; | ||
905 | } | ||
906 | else { | ||
907 | [REF_d_view setFrame:frame]; | ||
908 | } | ||
909 | } | ||
910 | |||
911 | void setTextChangedFunc_SystemTextInput(iSystemTextInput *d, | ||
912 | void (*textChangedFunc)(iSystemTextInput *, void *), | ||
913 | void *context) { | ||
914 | d->textChangedFunc = textChangedFunc; | ||
915 | d->textChangedContext = context; | ||
916 | } | ||
917 | |||
918 | static void notifyChange_SystemTextInput_(iSystemTextInput *d) { | ||
919 | if (d && d->textChangedFunc) { | ||
920 | d->textChangedFunc(d, d->textChangedContext); | ||
921 | } | ||
922 | } | ||
923 | |||
924 | static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *d) { | ||
925 | return (d->flags & insertNewlines_SystemTextInputFlag) != 0; | ||
926 | } | ||