From eddc86b7b0795fe7c7a82d86f6ee151ccdca229f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 6 Mar 2021 14:44:17 +0200 Subject: Mobile: Dealing with keyboard height The software keyboard obstructs part of the UI, so need to offset the view if the focused input widget would not be visible. --- src/app.c | 11 +++++--- src/ios.m | 69 +++++++++++++++++++++++++++++++++++++++------------ src/ui/inputwidget.c | 12 +++++++++ src/ui/lookupwidget.c | 23 ++++++++++------- src/ui/text.c | 20 ++++++++++++--- src/ui/text.h | 1 + src/ui/touch.c | 17 ++++++++++--- src/ui/widget.c | 16 ++++++++++++ src/ui/widget.h | 1 + src/ui/window.c | 20 +++++++++++++++ src/ui/window.h | 4 +++ 11 files changed, 160 insertions(+), 34 deletions(-) diff --git a/src/app.c b/src/app.c index a7539203..ada142f0 100644 --- a/src/app.c +++ b/src/app.c @@ -819,6 +819,11 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) { if (d->isIdling) { return iFalse; } +#endif +#if defined (iPlatformMobile) + if (!isFinished_Anim(&d->window->rootOffset)) { + return iFalse; + } #endif return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers); } @@ -929,7 +934,7 @@ void processEvents_App(enum iAppEventMode eventMode) { } } #if defined (LAGRANGE_IDLE_SLEEP) - if (d->isIdling && !gotEvents) { + if (d->isIdling && !gotEvents && isFinished_Anim(&d->window->rootOffset)) { /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we can't wait too long after the user tries to interact again with the app. In any case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */ @@ -1135,8 +1140,7 @@ iMimeHooks *mimeHooks_App(void) { } iBool isLandscape_App(void) { - const iApp *d = &app_; - const iInt2 size = rootSize_Window(d->window); + const iInt2 size = rootSize_Window(get_Window()); return size.x > size.y; } @@ -1700,6 +1704,7 @@ iBool handleCommand_App(const char *cmd) { #if defined (iPlatformAppleMobile) /* Can't close the last on mobile. */ if (tabCount_Widget(tabs) == 1) { + postCommand_App("navigate.home"); return iTrue; } #endif diff --git a/src/ios.m b/src/ios.m index 5abf87df..4d1aac37 100644 --- a/src/ios.m +++ b/src/ios.m @@ -31,28 +31,63 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ static iBool isSystemDarkMode_ = iFalse; static iBool isPhone_ = iFalse; +static UIViewController *viewController_(iWindow *window) { + SDL_SysWMinfo wm; + SDL_VERSION(&wm.version); + if (SDL_GetWindowWMInfo(window->win, &wm)) { + return wm.info.uikit.window.rootViewController; + } + iAssert(false); + return NULL; +} + +@interface KeyboardObserver : NSObject +-(void)keyboardOnScreen:(NSNotification *)notification; +@end + +@implementation KeyboardObserver +-(void)keyboardOnScreen:(NSNotification *)notification { + NSDictionary *info = notification.userInfo; + NSValue *value = info[UIKeyboardFrameEndUserInfoKey]; + CGRect rawFrame = [value CGRectValue]; + UIView *view = [viewController_(get_Window()) view]; + CGRect keyboardFrame = [view convertRect:rawFrame fromView:nil]; +// NSLog(@"keyboardFrame: %@", NSStringFromCGRect(keyboardFrame)); + iWindow *window = get_Window(); + const iInt2 rootSize = rootSize_Window(window); + const int keyTop = keyboardFrame.origin.y * window->pixelRatio; + setKeyboardHeight_Window(window, rootSize.y - keyTop); +} + +-(void)keyboardOffScreen:(NSNotification *)notification { + setKeyboardHeight_Window(get_Window(), 0); +} +@end + static void enableMouse_(iBool yes) { SDL_EventState(SDL_MOUSEBUTTONDOWN, yes); SDL_EventState(SDL_MOUSEMOTION, yes); SDL_EventState(SDL_MOUSEBUTTONUP, yes); } +KeyboardObserver *keyObs_; + void setupApplication_iOS(void) { enableMouse_(iFalse); NSString *deviceModel = [[UIDevice currentDevice] model]; if ([deviceModel isEqualToString:@"iPhone"]) { isPhone_ = iTrue; } -} - -static UIViewController *viewController_(iWindow *window) { - SDL_SysWMinfo wm; - SDL_VERSION(&wm.version); - if (SDL_GetWindowWMInfo(window->win, &wm)) { - return wm.info.uikit.window.rootViewController; - } - iAssert(false); - return NULL; + keyObs_ = [[KeyboardObserver alloc] init]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:keyObs_ + selector:@selector(keyboardOnScreen:) + name:UIKeyboardWillShowNotification + object:nil]; + [center addObserver:keyObs_ + selector:@selector(keyboardOffScreen:) + name:UIKeyboardWillHideNotification + object:nil]; } static iBool isDarkMode_(iWindow *window) { @@ -71,13 +106,15 @@ void safeAreaInsets_iOS(float *left, float *top, float *right, float *bottom) { UIViewController *ctl = viewController_(window); if (@available(iOS 11.0, *)) { const UIEdgeInsets safe = ctl.view.safeAreaInsets; - *left = safe.left * window->pixelRatio; - *top = safe.top * window->pixelRatio; - *right = safe.right * window->pixelRatio; - *bottom = safe.bottom * window->pixelRatio; + if (left) *left = safe.left * window->pixelRatio; + if (top) *top = safe.top * window->pixelRatio; + if (right) *right = safe.right * window->pixelRatio; + if (bottom) *bottom = safe.bottom * window->pixelRatio; } else { - // Fallback on earlier versions - *left = *top = *right = *bottom = 0.0f; + if (left) *left = 0.0f; + if (top) *top = 0.0f; + if (right) *right = 0.0f; + if (bottom) *bottom = 0.0f; } } diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 3f178fc5..06bb9474 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -21,6 +21,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "inputwidget.h" +#include "command.h" #include "paint.h" #include "util.h" #include "keys.h" @@ -615,6 +616,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { } return iFalse; } + else if (isCommand_UserEvent(ev, "keyboard.changed")) { + if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) { + iRect rect = bounds_Widget(w); + rect.pos.y -= value_Anim(&get_Window()->rootOffset); + const iInt2 visRoot = visibleRootSize_Window(get_Window()); + if (bottom_Rect(rect) > visRoot.y) { + setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250); + } + } + return iFalse; + } else if (isMetricsChange_UserEvent(ev)) { updateMetrics_InputWidget_(d); } diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c index 10039e10..eabfc7d2 100644 --- a/src/ui/lookupwidget.c +++ b/src/ui/lookupwidget.c @@ -36,6 +36,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "util.h" #include "visited.h" +#if defined (iPlatformAppleMobile) +# include "../ios.h" +#endif + #include #include #include @@ -641,8 +645,9 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) { if (isMetricsChange_UserEvent(ev)) { updateMetrics_LookupWidget_(d); } - else if (isResize_UserEvent(ev) || (equal_Command(cmd, "layout.changed") && - equal_Rangecc(range_Command(cmd, "id"), "navbar"))) { + else if (isResize_UserEvent(ev) || equal_Command(cmd, "keyboard.changed") || + (equal_Command(cmd, "layout.changed") && + equal_Rangecc(range_Command(cmd, "id"), "navbar"))) { /* Position the lookup popup under the URL bar. */ { const iWindow *window = get_Window(); const iInt2 rootSize = rootSize_Window(window); @@ -651,13 +656,13 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) { (rootSize.y - bottom_Rect(navBarBounds)) / 2)); setPos_Widget(w, bottomLeft_Rect(bounds_Widget(findWidget_App("url")))); #if defined (iPlatformAppleMobile) - /* TODO: Ask the system how tall the keyboard is. */ { - if (isLandscape_App()) { - w->rect.size.y = rootSize.y * 4 / 10; - } - else if (deviceType_App() == phone_AppDeviceType) { - w->rect.size.x = rootSize.x; - w->rect.pos.x = 0; + /* Adjust height based on keyboard size. */ { + w->rect.size.y = visibleRootSize_Window(window).y - top_Rect(bounds_Widget(w)); + if (deviceType_App() == phone_AppDeviceType) { + float l, r; + safeAreaInsets_iOS(&l, NULL, &r, NULL); + w->rect.size.x = rootSize.x - l - r; + w->rect.pos.x = l; } } #endif diff --git a/src/ui/text.c b/src/ui/text.c index d3b8df37..b08bdc60 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -53,6 +53,7 @@ int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */ int enableKerning_Text = iTrue; /* looking up kern pairs is slow */ static iBool enableRaster_Text_ = iTrue; +static int numPendingRasterization_Text_ = 0; enum iGlyphFlag { rasterized0_GlyphFlag = iBit(1), /* zero offset */ @@ -423,6 +424,7 @@ static void initCache_Text_(iText *d) { d->cacheSize.x, d->cacheSize.y); SDL_SetTextureBlendMode(d->cache, SDL_BLENDMODE_BLEND); + numPendingRasterization_Text_ = 0; } static void deinitCache_Text_(iText *d) { @@ -501,6 +503,10 @@ void resetFonts_Text(void) { initFonts_Text_(d); } +int numPendingGlyphs_Text(void) { + return numPendingRasterization_Text_; +} + iLocalDef iFont *font_Text_(enum iFontId id) { return &text_.fonts[id & mask_FontId]; } @@ -569,7 +575,7 @@ static void allocate_Font_(iFont *d, iGlyph *glyph, int hoff) { } } -static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) { +static iBool cache_Font_(iFont *d, iGlyph *glyph, int hoff) { iText * txt = &text_; SDL_Renderer *render = txt->render; SDL_Texture * tex = NULL; @@ -590,6 +596,7 @@ static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) { if (surface) { SDL_FreeSurface(surface); } + return isRasterized_Glyph_(glyph, hoff); } iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) { @@ -659,15 +666,22 @@ static const iGlyph *glyph_Font_(iFont *d, iChar ch) { allocate_Font_(font, glyph, 0); allocate_Font_(font, glyph, 1); insert_Hash(&font->glyphs, &glyph->node); + numPendingRasterization_Text_ += 2; } if (enableRaster_Text_ && !isFullyRasterized_Glyph_(glyph)) { SDL_Texture *oldTarget = SDL_GetRenderTarget(text_.render); SDL_SetRenderTarget(text_.render, text_.cache); if (!isRasterized_Glyph_(glyph, 0)) { - cache_Font_(font, glyph, 0); + if (cache_Font_(font, glyph, 0)) { + numPendingRasterization_Text_--; + iAssert(numPendingRasterization_Text_ >= 0); + } } if (!isRasterized_Glyph_(glyph, 1)) { - cache_Font_(font, glyph, 1); /* half-pixel offset */ + if (cache_Font_(font, glyph, 1)) { /* half-pixel offset */ + numPendingRasterization_Text_--; + iAssert(numPendingRasterization_Text_ >= 0); + } } SDL_SetRenderTarget(text_.render, oldTarget); } diff --git a/src/ui/text.h b/src/ui/text.h index f696b2e3..1118882d 100644 --- a/src/ui/text.h +++ b/src/ui/text.h @@ -161,6 +161,7 @@ void setContentFont_Text (enum iTextFont font); void setHeadingFont_Text (enum iTextFont font); void setContentFontSize_Text (float fontSizeFactor); /* affects all except `default*` fonts */ void resetFonts_Text (void); +int numPendingGlyphs_Text (void); int lineHeight_Text (int fontId); iInt2 measure_Text (int fontId, const char *text); diff --git a/src/ui/touch.c b/src/ui/touch.c index 27db0073..f15eda6f 100644 --- a/src/ui/touch.c +++ b/src/ui/touch.c @@ -190,6 +190,10 @@ static void update_TouchState_(void *ptr) { iForEach(Array, m, d->moms) { if (numSteps == 0) break; iMomentum *mom = m.value; + if (!mom->affinity) { + remove_ArrayIterator(&m); + continue; + } for (int step = 0; step < numSteps; step++) { mulvf_F3(&mom->velocity, momFriction); addv_F3(&mom->accum, mulf_F3(mom->velocity, stepDurationMs / 1000.0f)); @@ -248,8 +252,11 @@ static void dispatchButtonUp_Touch_(iFloat3 pos) { static iWidget *findOverflowScrollable_Widget_(iWidget *d) { const iInt2 rootSize = rootSize_Window(get_Window()); for (iWidget *w = d; w; w = parent_Widget(w)) { - if (flags_Widget(w) & overflowScrollable_WidgetFlag && height_Widget(w) > rootSize.y) { - return w; + if (flags_Widget(w) & overflowScrollable_WidgetFlag) { + if (height_Widget(w) > rootSize.y && !hasVisibleChildOnTop_Widget(w)) { + return w; + } + return NULL; } } return NULL; @@ -262,9 +269,13 @@ iBool processEvent_Touch(const SDL_Event *ev) { } iTouchState *d = touchState_(); iWindow *window = get_Window(); + if (!isFinished_Anim(&window->rootOffset)) { + return iFalse; + } const iInt2 rootSize = rootSize_Window(window); const SDL_TouchFingerEvent *fing = &ev->tfinger; - const iFloat3 pos = init_F3(fing->x * rootSize.x, fing->y * rootSize.y, 0); /* pixels */ + const iFloat3 pos = add_F3(init_F3(fing->x * rootSize.x, fing->y * rootSize.y, 0), /* pixels */ + init_F3(0, -value_Anim(&window->rootOffset), 0)); const uint32_t nowTime = SDL_GetTicks(); if (ev->type == SDL_FINGERDOWN) { /* Register the new touch. */ diff --git a/src/ui/widget.c b/src/ui/widget.c index c54ea444..2381548a 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -507,6 +507,9 @@ iRect bounds_Widget(const iWidget *d) { } addv_I2(&bounds.pos, pos); } +#if defined (iPlatformMobile) + bounds.pos.y += value_Anim(&get_Window()->rootOffset); +#endif return bounds; } @@ -1131,6 +1134,19 @@ static void printTree_Widget_(const iWidget *d, int indent) { } } +iBool hasVisibleChildOnTop_Widget(const iWidget *parent) { + iConstForEach(ObjectList, i, parent->children) { + const iWidget *child = i.object; + if (~child->flags & hidden_WidgetFlag && child->flags & keepOnTop_WidgetFlag) { + return iTrue; + } + if (hasVisibleChildOnTop_Widget(child)) { + return iTrue; + } + } + return iFalse; +} + void printTree_Widget(const iWidget *d) { printTree_Widget_(d, 0); } diff --git a/src/ui/widget.h b/src/ui/widget.h index b1ee8c73..602e86cb 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -232,6 +232,7 @@ iWidget *hover_Widget (void); void unhover_Widget (void); void setMouseGrab_Widget (iWidget *); iWidget *mouseGrab_Widget (void); +iBool hasVisibleChildOnTop_Widget(const iWidget *parent); void printTree_Widget (const iWidget *); iBool equalWidget_Command (const char *cmd, const iWidget *widget, const char *checkCommand); diff --git a/src/ui/window.c b/src/ui/window.c index bf7214f4..9ff22d35 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1136,6 +1136,7 @@ static void updateRootSize_Window_(iWindow *d, iBool notifyAlways) { iInt2 *size = &d->root->rect.size; const iInt2 oldSize = *size; SDL_GetRendererOutputSize(d->render, &size->x, &size->y); + size->y -= d->keyboardHeight; if (notifyAlways || !isEqual_I2(oldSize, *size)) { const iBool isHoriz = (d->place.lastNotifiedSize.x != size->x); const iBool isVert = (d->place.lastNotifiedSize.y != size->y); @@ -1287,6 +1288,8 @@ void init_Window(iWindow *d, iRect rect) { d->isMouseInside = iTrue; d->ignoreClick = iFalse; d->focusGainedAt = 0; + d->keyboardHeight = 0; + init_Anim(&d->rootOffset, 0.0f); uint32_t flags = 0; #if defined (iPlatformAppleDesktop) SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl"); @@ -1704,6 +1707,7 @@ void draw_Window(iWindow *d) { if (d->isDrawFrozen) { return; } +// printf("num pending: %d\n", numPendingGlyphs_Text()); const int winFlags = SDL_GetWindowFlags(d->win); const iBool gotFocus = (winFlags & SDL_WINDOW_INPUT_FOCUS) != 0; /* Clear the window. The clear color is visible as a border around the window @@ -1797,6 +1801,10 @@ iInt2 rootSize_Window(const iWindow *d) { return d ? d->root->rect.size : zero_I2(); } +iInt2 visibleRootSize_Window(const iWindow *d) { + return addY_I2(rootSize_Window(d), -d->keyboardHeight); +} + iInt2 coord_Window(const iWindow *d, int x, int y) { #if defined (iPlatformMsys) || defined (iPlatformLinux) /* On Windows, surface coordinates are in pixels. */ @@ -1828,6 +1836,18 @@ iWindow *get_Window(void) { return theWindow_; } +void setKeyboardHeight_Window(iWindow *d, int height) { + if (d->keyboardHeight != height) { + d->keyboardHeight = height; + if (height == 0) { + setFlags_Anim(&d->rootOffset, easeBoth_AnimFlag, iTrue); + setValue_Anim(&d->rootOffset, 0, 250); + } + postCommandf_App("keyboard.changed arg:%d", height); + postRefresh_App(); + } +} + void setSnap_Window(iWindow *d, int snapMode) { if (!prefs_App()->customFrame) { if (snapMode == maximized_WindowSnap) { diff --git a/src/ui/window.h b/src/ui/window.h index 7cd29d4b..8bc9911c 100644 --- a/src/ui/window.h +++ b/src/ui/window.h @@ -75,6 +75,8 @@ struct Impl_Window { SDL_Cursor * cursors[SDL_NUM_SYSTEM_CURSORS]; SDL_Cursor * pendingCursor; int loadAnimTimer; + iAnim rootOffset; + int keyboardHeight; /* mobile software keyboards */ }; iBool processEvent_Window (iWindow *, const SDL_Event *); @@ -86,9 +88,11 @@ void setUiScale_Window (iWindow *, float uiScale); void setFreezeDraw_Window (iWindow *, iBool freezeDraw); void setCursor_Window (iWindow *, int cursor); void setSnap_Window (iWindow *, int snapMode); +void setKeyboardHeight_Window(iWindow *, int height); uint32_t id_Window (const iWindow *); iInt2 rootSize_Window (const iWindow *); +iInt2 visibleRootSize_Window (const iWindow *); /* may be obstructed by software keyboard */ float uiScale_Window (const iWindow *); iInt2 coord_Window (const iWindow *, int x, int y); iInt2 mouseCoord_Window (const iWindow *); -- cgit v1.2.3