html5: fix ACTIVATE/DEACTIVATE getting stuck when a tab is hidden without a focus/blur pair (common on mobile Safari) - #2104
Open
mallyskies wants to merge 2 commits into
Conversation
…E already handle handleWindowEvent() has had a "visibilitychange" case since openfl#1401 (closed, never merged), reimplemented in d918341. That commit kept the switch arm and the `hidden` latch but dropped the addEventListener call, so the case has been dead code since 2020: ACTIVATE/DEACTIVATE could only ever follow a real window focus/blur pair. That misses any tab-hidden transition that doesn't fire focus/blur on this window, e.g. opening a browser tab overview or switching tabs by a gesture rather than a direct click on another window. An app embedded in such a page can DEACTIVATE-suspend on the way out and then never see anything to ACTIVATE-resume it on the way back, since no focus event follows either. visibilitychange is a document event (Page Visibility API), not a window event, so it is registered on Browser.document alongside the window-level focus/blur/resize/beforeunload listeners already above it.
WebKit can leave a page's focus state stale after the user returns to it -
observed on iOS Safari returning from the tab overview ('All Tabs'), where
the page never receives a 'focus' DOM event even though it is frontmost and
receiving input again. lime's own ACTIVATE/DEACTIVATE tracking has no way to
notice: it only clears the internal `hidden` latch on a real 'focus' event
(or the 'visibilitychange' case, previous commit), so an app that suspended
itself on DEACTIVATE stays suspended indefinitely with no further event ever
arriving to resume it.
A mousedown/touchstart reaching the window's own element cannot happen
unless the window is frontmost, so treat it as an activation signal in its
own right rather than waiting on a 'focus' event that may never come. This
matters most for touchstart: HTML5Window already calls preventDefault() on
it, which on WebKit also suppresses the synthetic click some engines use
internally to notice a backgrounded page regained focus - so a canvas that
only ever receives touches (true of most mobile play) could otherwise never
leave the hidden state again after the first time it was backgrounded.
Refactors the two existing 'became visible' branches in handleWindowEvent()
('focus', and the visible half of 'visibilitychange') to share the same
activate() this adds, rather than duplicating the same three lines a third
time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HTML5Application's ACTIVATE/DEACTIVATE tracking can get stuck DEACTIVATE forever, with nothing left to fire ACTIVATE:handleWindowEvent()has had a"visibilitychange"case since Visibility-change API implementation added #1401 (closed, unmerged) / d918341, but nothing ever callsaddEventListenerfor it (i.e. dead code since 2020). Anything hiding the tab without ablur/focuspair on the window (e.g. a tab-overview UI vs. a click into another window) never DEACTIVATEs, or DEACTIVATEs and never gets ACTIVATE back.blurgoing out and nothing coming back: nofocus, novisibilitychange. The page is frontmost and receiving touches, buthasFocus()returns false. Further,HTML5Window'spreventDefault()ontouchstartblocks the synthetic click some engines use internally to notice a backgrounded page regained focus, so a touch-only canvas can never leave the hidden state again after the first backgrounding.Real-world impact
An app that suspends on DEACTIVATE (pause, mute, stop a timer) and expects ACTIVATE to resume it can become suspended for the rest of the session. We ran into this with one of our game apps that hides the game progress when not active.
Fix
visibilitychangelistener as per Page Visibility API.mousedown/touchstartreaching the window's element as proof it's active, regardless of whathiddensays, since these events can't happen unless the window is frontmost. Refactors the two existing "became visible" branches into oneactivate()so this doesn't duplicate them a third time.Testing
project.xml:src/Main.hx:lime build html5 -debug, openhtml5/bin/index.html, then in the browser console, paste:Outputs
["DEACTIVATE"]on unpatched develop, and with the fix in place displays:["DEACTIVATE", "ACTIVATE"].Same process for the touch path. Reload, then:
Outputs
["DEACTIVATE"]on unpatched develop, and with the fix in place displays:["DEACTIVATE", "ACTIVATE"].