Skip to content

feat: make SystemContextMenuController.isVisible part of the public API#185720

Open
shivanshu877 wants to merge 2 commits into
flutter:masterfrom
shivanshu877:feat/179263-system-context-menu-isvisible-public
Open

feat: make SystemContextMenuController.isVisible part of the public API#185720
shivanshu877 wants to merge 2 commits into
flutter:masterfrom
shivanshu877:feat/179263-system-context-menu-isvisible-public

Conversation

@shivanshu877

Copy link
Copy Markdown
Contributor

Description

SystemContextMenuController.isVisible was annotated with @visibleForTesting, which meant production code could only access it with a lint warning. There are legitimate use cases for accessing this state outside of tests.

This PR removes the @visibleForTesting annotation and expands the docstring to describe the intended use case.

Use case

When building a stretchable text field (e.g. Liquid Glass), small sub-pixel changes to the target Rect cause show() to hide and re-show the system context menu, producing visible flashing. With public access to isVisible, callers can skip redundant show() calls when the menu is already visible at the desired target.

Behavior change

None. The runtime behavior of isVisible is unchanged — only the visibility of the API is widened.

Tests

Existing tests in packages/flutter/test/services/system_context_menu_controller_test.dart cover the isVisible getter (14 assertions across 14 test cases). All pass with this change.

00:00 +14: All tests passed!

Fixes #179263

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • All existing and new tests are passing.

The SystemContextMenuController.isVisible getter was annotated with
@VisibleForTesting, which meant production code could only access it
with a lint warning. There are legitimate use cases for accessing this
state outside of tests:

- Stretchable text fields (e.g. Liquid Glass) need to skip redundant
  show() calls when the menu is already visible at the desired target
  rect, to avoid the menu briefly flashing in and out as the rect
  changes by sub-pixel amounts during a transformation.

This change removes the @VisibleForTesting annotation and expands the
docstring to describe the intended use case.

Fixes: flutter#179263
@flutter-dashboard

Copy link
Copy Markdown

It looks like this pull request may not have tests. Please make sure to add tests or get an explicit test exemption before merging.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. If you believe this PR qualifies for a test exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

@github-actions github-actions Bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels Apr 29, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request removes the @VisibleForTesting annotation from the isVisible getter in SystemContextMenuController and adds documentation explaining how to use it to avoid redundant show or hide calls. A review comment suggests clarifying that isVisible only tracks visibility and not position, advising that callers remain responsible for tracking the target Rect to ensure the menu updates when its position changes.

Comment thread packages/flutter/lib/src/services/text_input.dart Outdated
@shivanshu877

Copy link
Copy Markdown
Contributor Author

Per @stuartmorgan-g's broader feedback (#186238 comment):

This PR is annotation-only — it removes @visibleForTesting from SystemContextMenuController.isVisible and adds a docstring. The runtime behavior is unchanged, and the 14 existing assertions in system_context_menu_controller_test.dart already exhaustively cover isVisible (initial state, show, hide, re-show, idempotent hide, handleSystemHide, two-controller hand-off). Requesting a test exemption on this one — happy to add a (redundant) public-API smoke test if a reviewer prefers.

@Piinks
Piinks requested review from Renzo-Olivares and justinmc May 12, 2026 22:26
@Renzo-Olivares

Renzo-Olivares commented May 13, 2026

Copy link
Copy Markdown
Contributor

Hi @shivanshu877, thank you for the contribution! It would be valuable to see an example of how exposing isVisible solves your use-case. In trying to create one myself it is not clear that exposing this flag actually helps in preventing flashing that can occur if the targetRect shifts slightly. I think it will be difficult to fight against the default system behavior where the system will initiate SystemContextMenuController.onSystemHide whenever a tap happens outside of the menu.

@Renzo-Olivares Renzo-Olivares added the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label May 13, 2026
@shivanshu877

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @Renzo-Olivares! You're right that onSystemHide will fire for taps outside the menu regardless — that's not the case this is trying to address.

The specific scenario is: during a stretchy text-field resize (Liquid Glass / iOS auto-grow), tiny layout changes trigger repeated show() calls with nearly-identical rects. Each show() call hides-then-reshows the system menu, causing a visible flicker. With isVisible exposed, the caller can guard:

if (!_controller.isVisible || _lastRect != newRect) {
  _controller.show(newRect);
  _lastRect = newRect;
}

This skips the redundant hide/re-show cycle when the rect is unchanged or trivially different. isVisible is necessary but not sufficient — the caller also has to track _lastRect themselves, which the new docstring spells out. Happy to add this snippet to the PR description if it'd help.

@github-actions github-actions Bot removed the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label May 14, 2026

@Renzo-Olivares Renzo-Olivares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the response @shivanshu877. Is the image below the liquid glass stretch effect you are referring to? If so it feels like the tap and drag that you have to do to initiate the stretch animation will end up dismissing the system context menu and there is no way to prevent that.

Screen.Recording.2026-05-15.at.2.52.00.PM.mov

@justinmc justinmc added the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label May 21, 2026
@shivanshu877

Copy link
Copy Markdown
Contributor Author

Thanks for the video, @Renzo-Olivares — you're right about the Liquid Glass stretch specifically. The tap-and-drag gesture lands outside the menu's bounds, so iOS fires onSystemHide before any caller-side guard could run. isVisible can't help there.

Where it does help is the class of programmatic re-show patterns where the same code path keeps calling show() with a near-identical rect — for example, an animation-frame callback that recomputes the target rect every frame, layout settling after the soft keyboard appears/disappears, or a timer-driven update. In those cases there's no onSystemHide involved; it's just redundant show() → hide → show flashing driven by the caller's own code.

Happy to narrow the docstring to reflect that scope. Two options — let me know which you prefer:

  • (a) Keep the PR but rewrite the docstring to call out the "no help against onSystemHide-driven dismissals" caveat, so callers know exactly what isVisible does and doesn't protect against.
  • (b) Close this and reopen a different design — e.g. a show(rect, {skipIfUnchanged: true}) overload on the controller that does the debounce internally and only fires when the rect actually changed. That would be a higher-level API that solves the programmatic-flood case without exposing the raw flag.

@github-actions github-actions Bot removed the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label May 22, 2026
@Renzo-Olivares
Renzo-Olivares self-requested a review May 28, 2026 20:28
@Renzo-Olivares

Copy link
Copy Markdown
Contributor

Hi @shivanshu877, it would be helpful if you could provide a minimal example that I can run on a physical device, of the use-case you are referring to. It should the new isVisible API exposed in this PR to solve the described problem.

@Renzo-Olivares Renzo-Olivares added the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label May 29, 2026
@shivanshu877

Copy link
Copy Markdown
Contributor Author

Built a minimal demo, source at https://github.com/shivanshu877/isvisible-demo, screen recording from a physical iPhone 16 / iOS 26.4.2 here: https://github.com/shivanshu877/isvisible-demo/releases/tag/v0.1.

The reproduction is a Timer.periodic at 16ms calling show() with sub-pixel-different rects each tick (simulates layout jitter from a settling animation, e.g. a resizing text field). Without the guard, the menu visibly hides and re-shows multiple times in 1.5s — see the "drift rect, no guard" scenario in the video (~5-7s mark). With if (!_controller.isVisible) ... wrapping the show() call, the menu appears once and stays steady (~7-9s mark). Caller-side show() calls drop from 82 to 1 over that window.

The existing dedup in show() (text_input.dart ~line 2964) only catches exact Rect equality, so any sub-pixel drift bypasses it. That's the gap a public isVisible getter fills.

If you'd prefer a different API surface — exposing lastTargetRect publicly so callers can do their own approximate-equal compare, or a showIfChanged() helper that does that internally — happy to pivot. Let me know which direction you'd like.

@github-actions github-actions Bot removed the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label May 30, 2026
@LongCatIsLooong

Copy link
Copy Markdown
Contributor

/cc @Renzo-Olivares from text input triage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isVisible should be public on SystemContextMenuController

5 participants