diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-03-07 08:15:57 +0200 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-03-07 08:15:57 +0200 |
commit | 4562609999fb880f66ee0eccef409d69a64a4ce0 (patch) | |
tree | 8fcdc34cbde9e837136f29259da4cdde7884bd5d | |
parent | 5b80af0e983db5e3d09e207c9fce6790b53dea4d (diff) |
iOS: Haptic tap when long-pressing
-rw-r--r-- | src/ios.h | 5 | ||||
-rw-r--r-- | src/ios.m | 123 | ||||
-rw-r--r-- | src/ui/touch.c | 7 |
3 files changed, 126 insertions, 9 deletions
@@ -26,8 +26,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
26 | 26 | ||
27 | iDeclareType(Window) | 27 | iDeclareType(Window) |
28 | 28 | ||
29 | enum iHapticEffect { | ||
30 | tap_HapticEffect, | ||
31 | }; | ||
32 | |||
29 | void setupApplication_iOS (void); | 33 | void setupApplication_iOS (void); |
30 | void setupWindow_iOS (iWindow *window); | 34 | void setupWindow_iOS (iWindow *window); |
31 | iBool isPhone_iOS (void); | 35 | iBool isPhone_iOS (void); |
32 | void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); | 36 | void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); |
33 | iBool processEvent_iOS (const SDL_Event *); | 37 | iBool processEvent_iOS (const SDL_Event *); |
38 | void playHapticEffect_iOS (enum iHapticEffect effect); | ||
@@ -22,14 +22,16 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
22 | 22 | ||
23 | #include "ios.h" | 23 | #include "ios.h" |
24 | #include "app.h" | 24 | #include "app.h" |
25 | #include "ui/command.h" | ||
25 | #include "ui/window.h" | 26 | #include "ui/window.h" |
26 | #include <SDL_events.h> | 27 | #include <SDL_events.h> |
27 | #include <SDL_syswm.h> | 28 | #include <SDL_syswm.h> |
28 | 29 | ||
29 | #import <UIKit/UIKit.h> | 30 | #import <UIKit/UIKit.h> |
31 | #import <CoreHaptics/CoreHaptics.h> | ||
30 | 32 | ||
31 | static iBool isSystemDarkMode_ = iFalse; | 33 | static iBool isSystemDarkMode_ = iFalse; |
32 | static iBool isPhone_ = iFalse; | 34 | static iBool isPhone_ = iFalse; |
33 | 35 | ||
34 | static UIViewController *viewController_(iWindow *window) { | 36 | static UIViewController *viewController_(iWindow *window) { |
35 | SDL_SysWMinfo wm; | 37 | SDL_SysWMinfo wm; |
@@ -41,11 +43,104 @@ static UIViewController *viewController_(iWindow *window) { | |||
41 | return NULL; | 43 | return NULL; |
42 | } | 44 | } |
43 | 45 | ||
44 | @interface KeyboardObserver : NSObject | 46 | /*----------------------------------------------------------------------------------------------*/ |
45 | -(void)keyboardOnScreen:(NSNotification *)notification; | 47 | |
48 | API_AVAILABLE(ios(13.0)) | ||
49 | @interface HapticState : NSObject | ||
50 | @property (nonatomic, strong) CHHapticEngine *engine; | ||
51 | @property (nonatomic, strong) NSDictionary *tapDef; | ||
52 | @end | ||
53 | |||
54 | @implementation HapticState | ||
55 | |||
56 | -(void)setup { | ||
57 | NSError *error; | ||
58 | self.engine = [[CHHapticEngine alloc] initAndReturnError:&error]; | ||
59 | __weak HapticState *hs = self; | ||
60 | [self.engine setResetHandler:^{ | ||
61 | NSLog(@"Haptic engine reset"); | ||
62 | NSError *startupError; | ||
63 | [hs.engine startAndReturnError:&startupError]; | ||
64 | if (startupError) { | ||
65 | NSLog(@"Engine couldn't restart"); | ||
66 | } | ||
67 | else { | ||
68 | // TODO: Create pattern players. | ||
69 | } | ||
70 | }]; | ||
71 | [self.engine setStoppedHandler:^(CHHapticEngineStoppedReason reason){ | ||
72 | NSLog(@"Haptic engine stopped"); | ||
73 | switch (reason) { | ||
74 | case CHHapticEngineStoppedReasonAudioSessionInterrupt: | ||
75 | break; | ||
76 | case CHHapticEngineStoppedReasonApplicationSuspended: | ||
77 | break; | ||
78 | case CHHapticEngineStoppedReasonIdleTimeout: | ||
79 | break; | ||
80 | case CHHapticEngineStoppedReasonSystemError: | ||
81 | break; | ||
82 | default: | ||
83 | break; | ||
84 | } | ||
85 | }]; | ||
86 | self.tapDef = @{ | ||
87 | CHHapticPatternKeyPattern: | ||
88 | @[ | ||
89 | @{ | ||
90 | CHHapticPatternKeyEvent: @{ | ||
91 | CHHapticPatternKeyEventType: CHHapticEventTypeHapticTransient, | ||
92 | CHHapticPatternKeyTime: @0.0, | ||
93 | CHHapticPatternKeyEventDuration:@0.1 | ||
94 | }, | ||
95 | }, | ||
96 | ], | ||
97 | }; | ||
98 | } | ||
99 | |||
100 | -(void)playTapEffect { | ||
101 | NSError *error = nil; | ||
102 | CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:self.tapDef | ||
103 | error:&error]; | ||
104 | // TODO: Check the error. | ||
105 | id<CHHapticPatternPlayer> player = [self.engine createPlayerWithPattern:pattern error:&error]; | ||
106 | // TODO: Check the error. | ||
107 | [self.engine startWithCompletionHandler:^(NSError *err){ | ||
108 | if (err == nil) { | ||
109 | [self.engine notifyWhenPlayersFinished:^(NSError * _Nullable error) { | ||
110 | return CHHapticEngineFinishedActionStopEngine; | ||
111 | }]; | ||
112 | NSError *startError = nil; | ||
113 | [player startAtTime:0.0 error:&startError]; | ||
114 | } | ||
115 | }]; | ||
116 | } | ||
117 | |||
118 | @end | ||
119 | |||
120 | /*----------------------------------------------------------------------------------------------*/ | ||
121 | |||
122 | @interface AppState : NSObject | ||
123 | @property (nonatomic, assign) BOOL isHapticsAvailable; | ||
124 | @property (nonatomic, strong) NSObject *haptic; | ||
46 | @end | 125 | @end |
47 | 126 | ||
48 | @implementation KeyboardObserver | 127 | static AppState *appState_; |
128 | |||
129 | @implementation AppState | ||
130 | |||
131 | -(void)setupHaptics { | ||
132 | if (@available(iOS 13.0, *)) { | ||
133 | self.isHapticsAvailable = CHHapticEngine.capabilitiesForHardware.supportsHaptics; | ||
134 | if (self.isHapticsAvailable) { | ||
135 | HapticState *hs = [[HapticState alloc] init]; | ||
136 | [hs setup]; | ||
137 | self.haptic = hs; | ||
138 | } | ||
139 | } else { | ||
140 | self.isHapticsAvailable = NO; | ||
141 | } | ||
142 | } | ||
143 | |||
49 | -(void)keyboardOnScreen:(NSNotification *)notification { | 144 | -(void)keyboardOnScreen:(NSNotification *)notification { |
50 | NSDictionary *info = notification.userInfo; | 145 | NSDictionary *info = notification.userInfo; |
51 | NSValue *value = info[UIKeyboardFrameEndUserInfoKey]; | 146 | NSValue *value = info[UIKeyboardFrameEndUserInfoKey]; |
@@ -70,21 +165,20 @@ static void enableMouse_(iBool yes) { | |||
70 | SDL_EventState(SDL_MOUSEBUTTONUP, yes); | 165 | SDL_EventState(SDL_MOUSEBUTTONUP, yes); |
71 | } | 166 | } |
72 | 167 | ||
73 | KeyboardObserver *keyObs_; | ||
74 | |||
75 | void setupApplication_iOS(void) { | 168 | void setupApplication_iOS(void) { |
76 | enableMouse_(iFalse); | 169 | enableMouse_(iFalse); |
77 | NSString *deviceModel = [[UIDevice currentDevice] model]; | 170 | NSString *deviceModel = [[UIDevice currentDevice] model]; |
78 | if ([deviceModel isEqualToString:@"iPhone"]) { | 171 | if ([deviceModel isEqualToString:@"iPhone"]) { |
79 | isPhone_ = iTrue; | 172 | isPhone_ = iTrue; |
80 | } | 173 | } |
81 | keyObs_ = [[KeyboardObserver alloc] init]; | 174 | appState_ = [[AppState alloc] init]; |
175 | [appState_ setupHaptics]; | ||
82 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; | 176 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; |
83 | [center addObserver:keyObs_ | 177 | [center addObserver:appState_ |
84 | selector:@selector(keyboardOnScreen:) | 178 | selector:@selector(keyboardOnScreen:) |
85 | name:UIKeyboardWillShowNotification | 179 | name:UIKeyboardWillShowNotification |
86 | object:nil]; | 180 | object:nil]; |
87 | [center addObserver:keyObs_ | 181 | [center addObserver:appState_ |
88 | selector:@selector(keyboardOffScreen:) | 182 | selector:@selector(keyboardOffScreen:) |
89 | name:UIKeyboardWillHideNotification | 183 | name:UIKeyboardWillHideNotification |
90 | object:nil]; | 184 | object:nil]; |
@@ -128,6 +222,17 @@ void setupWindow_iOS(iWindow *window) { | |||
128 | postCommandf_App("~os.theme.changed dark:%d contrast:1", isSystemDarkMode_ ? 1 : 0); | 222 | postCommandf_App("~os.theme.changed dark:%d contrast:1", isSystemDarkMode_ ? 1 : 0); |
129 | } | 223 | } |
130 | 224 | ||
225 | void playHapticEffect_iOS(enum iHapticEffect effect) { | ||
226 | if (@available(iOS 13.0, *)) { | ||
227 | HapticState *hs = (HapticState *) appState_.haptic; | ||
228 | switch(effect) { | ||
229 | case tap_HapticEffect: | ||
230 | [hs playTapEffect]; | ||
231 | break; | ||
232 | } | ||
233 | } | ||
234 | } | ||
235 | |||
131 | iBool processEvent_iOS(const SDL_Event *ev) { | 236 | iBool processEvent_iOS(const SDL_Event *ev) { |
132 | if (ev->type == SDL_WINDOWEVENT) { | 237 | if (ev->type == SDL_WINDOWEVENT) { |
133 | if (ev->window.event == SDL_WINDOWEVENT_RESTORED) { | 238 | if (ev->window.event == SDL_WINDOWEVENT_RESTORED) { |
diff --git a/src/ui/touch.c b/src/ui/touch.c index f15eda6f..136457a5 100644 --- a/src/ui/touch.c +++ b/src/ui/touch.c | |||
@@ -28,6 +28,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
28 | #include <the_Foundation/math.h> | 28 | #include <the_Foundation/math.h> |
29 | #include <SDL_timer.h> | 29 | #include <SDL_timer.h> |
30 | 30 | ||
31 | #if defined (iPlatformAppleMobile) | ||
32 | # include "../ios.h" | ||
33 | #endif | ||
34 | |||
31 | iDeclareType(Touch) | 35 | iDeclareType(Touch) |
32 | iDeclareType(TouchState) | 36 | iDeclareType(TouchState) |
33 | iDeclareType(Momentum) | 37 | iDeclareType(Momentum) |
@@ -174,6 +178,9 @@ static void update_TouchState_(void *ptr) { | |||
174 | touch->isTapAndHold = iTrue; | 178 | touch->isTapAndHold = iTrue; |
175 | touch->hasMoved = iFalse; | 179 | touch->hasMoved = iFalse; |
176 | touch->startPos = touch->pos[0]; | 180 | touch->startPos = touch->pos[0]; |
181 | #if defined (iPlatformAppleMobile) | ||
182 | playHapticEffect_iOS(tap_HapticEffect); | ||
183 | #endif | ||
177 | } | 184 | } |
178 | } | 185 | } |
179 | } | 186 | } |