Skip to content

Fix textscaler clamp assertion error#181716

Merged
auto-submit[bot] merged 4 commits intoflutter:masterfrom
Valansch:fix-textscaler-clamp-assertion
Feb 24, 2026
Merged

Fix textscaler clamp assertion error#181716
auto-submit[bot] merged 4 commits intoflutter:masterfrom
Valansch:fix-textscaler-clamp-assertion

Conversation

@Valansch
Copy link
Contributor

@Valansch Valansch commented Jan 30, 2026

Fix TextScaler.clamp assertion failure when clamping already-clamped scalers

Fixes #174828

Description

When calling clamp() on an already-clamped TextScaler, the _ClampedTextScaler.clamp() method was checking if minScaleFactor == maxScaleFactor before calculating the actual new min/max values. This caused assertion failures ('maxScale > minScale': is not true) when the calculated newMinScale and newMaxScale became equal after applying max()/min() operations.

Example scenario that triggers the bug:

final textScaler = MediaQuery.of(context).textScaler;
// First clamp - creates a _ClampedTextScaler
textScaler.clamp(minScaleFactor: 1.0, maxScaleFactor: 1.1);
// Internal framework code calls clamp() again (e.g., in BottomNavigationBar)
// This could result in newMinScale == newMaxScale, triggering the assertion

The fix:

Calculate newMinScale and newMaxScale first, then check for equality. If they are equal, return a _LinearTextScaler instead of creating a _ClampedTextScaler that would fail its constructor assertion.

Important

This breaks before undefined behaviour. We now through an assertion error when new bounds are out of bounds even when min and max are equal. For more detail view my comment on this here

@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 Jan 30, 2026
Copy link
Contributor

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

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 correctly fixes an assertion error in TextScaler.clamp when called on an already-clamped scaler. The logic is now more robust by calculating the new clamp boundaries before deciding which type of scaler to return. The added tests effectively cover the bug fix and a related edge case. My only feedback is a suggestion to improve the organization of the new tests for better maintainability, as detailed in the comment.

@justinmc justinmc requested a review from victorsanni February 3, 2026 23:31
@Valansch Valansch force-pushed the fix-textscaler-clamp-assertion branch 2 times, most recently from 48b4b5b to 8afaeec Compare February 4, 2026 13:29
Copy link
Contributor

@victorsanni victorsanni left a comment

Choose a reason for hiding this comment

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

Can you run dart format on all files in this PR to fix failing checks?

@Valansch
Copy link
Contributor Author

Valansch commented Feb 4, 2026

@victorsanni

Can you run dart format on all files in this PR to fix failing checks?

I don't understand why it would be failing. Looks good to me. Is using dart from /bin/ incorrect? Which version should I use. Sorry if I missed some instructions.

./bin/dart format packages/flutter/
Formatted 1685 files (0 changed) in 12.18 seconds

EDIT: Nevermind. Analyze caught it.

@Valansch
Copy link
Contributor Author

Valansch commented Feb 4, 2026

@victorsanni nevermind my last comment. Flutter analyze should have caught the issue. I addressed your comments. Will squash the second commit after review.

Copy link
Contributor

@victorsanni victorsanni left a comment

Choose a reason for hiding this comment

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

To fix, run dart format packages/flutter/test/painting/text_scaler_test.dart

@Valansch Valansch force-pushed the fix-textscaler-clamp-assertion branch 2 times, most recently from 0d5b1e8 to 0940969 Compare February 5, 2026 09:01
@Valansch
Copy link
Contributor Author

Valansch commented Feb 5, 2026

@victorsanni Resolved

@Valansch Valansch requested a review from victorsanni February 5, 2026 09:02
return minScaleFactor == maxScaleFactor
? _LinearTextScaler(minScaleFactor)
: _ClampedTextScaler(scaler, max(minScaleFactor, minScale), min(maxScaleFactor, maxScale));
: _ClampedTextScaler(scaler, minScaleFactor, maxScaleFactor);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is correct. When you compose 2 clampers the first one shouldn't be ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

View this comment and make up you mind you two :)
#174828 (comment)

I implemented the fix first respecting the old bounds. Then i was told to ignore them, now not?

Copy link
Contributor

@LongCatIsLooong LongCatIsLooong Feb 5, 2026

Choose a reason for hiding this comment

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

Ah I thought by "discard" @victorsanni meant the this.minScale and this.maxScale should be removed from the expression. But with this implementation I think the first clamp is just going to be ignored and that would create some surprising behaviors. I would expect text scalers compose like functions so clamp2 . clamp1 === clamp2 . identityScaler . clamp1 (where identifyScaler is just a text scaler that returns the input text size as-is).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But what do you do it the clamps are incompatible? Please view my comment on the issue i explained my thoughts there

Copy link
Contributor

Choose a reason for hiding this comment

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

If clamp1 has a range of [1, 2] and clamp2 [3, 4], what should the output size be when the input font size 9 goes through clamp1 and then clamp2? I'm expecting 3 instead of 4 (but I think this implementation would return 4).

The developer doesn't know the runtime type of the TextScaler they're calling clamp on (at least in most cases). And with this implementation I believe when you call clamp the exact behavior will depend on the runtime type of the receiver, if the receiver is a _ClampTextScaler then the receiver's scaling logic will be ignored but if it's not then the clamping will be applied after the receiver's scaling logic is applied.

Copy link
Contributor Author

@Valansch Valansch Feb 9, 2026

Choose a reason for hiding this comment

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

Thinking about this again: My current approach, as the previous (bugged) version is commutative. But clamping is not. So the current approach is not working. I will add another test and consider your suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to implement your approach to just nest the clamedScalers like this:

  @override
  TextScaler clamp({double minScaleFactor = 0, double maxScaleFactor = double.infinity}) {
    return minScaleFactor == maxScaleFactor
        ? TextScaler.linear(minScaleFactor)
        : _ClampedTextScaler(this, minScaleFactor, maxScaleFactor);
  }

Unfortunately that breaks equality, because only the last clamps are compared. Now I can fix the equality by implementing a numeric solution to this problem, but then why not just implement the numeric solution directly in the clamp and don't nest? This is in addition to my points raised above.

Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately that breaks equality, because only the last clamps are compared

Could you elaborate? The "inner" scaler is also compared in the == implementation. Also it's correct to always return false for TextScaler.== implementations because it's just an optimization to prevent unnecessary rebuilds. Function equality is inherently tricky so the documentation only asks that subclasses should identify "equal" text scalers on a best effort basis.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nesting the clamed scalers can result in the same scalers not being equal. Example:

final scaler = TexScaler.linear(2)
scaler.clamp(2,3) != scaler.clamp(1,4).clamp(2,3)

scaler.clamp(1,5).clamp(2,3) != scaler.clamp(1,4).clamp(2,3)

I believe those should be equal.

Copy link
Contributor

Choose a reason for hiding this comment

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

TextScaler.== is only used by the framework to skip unnecessary widget rebuilds so you can always choose to return false, it's totally optional. But if you really want to prevent rebuilds as much as possible I think you can switch to the original implementation (union of the two clamp intervals) when the intervals overlap.

@Valansch Valansch force-pushed the fix-textscaler-clamp-assertion branch 2 times, most recently from e31a48d to 5517a32 Compare February 5, 2026 23:47
@Valansch Valansch force-pushed the fix-textscaler-clamp-assertion branch from 5517a32 to b7e7914 Compare February 10, 2026 16:27
@Valansch
Copy link
Contributor Author

@LongCatIsLooong @victorsanni I implemented a new version now. Please mind my response to your comments under the comment.
I have also extended the test, it now tests all cases and equality.

If you have a change suggestion I can try it against this test. I believe this is the best version. I removed the ternary to add a comment.


if (newMaxScale <= newMinScale) {
// Ranges don't overlap or collapse to a single point.
return TextScaler.linear(clampDouble(minScale, minScaleFactor, maxScaleFactor));
Copy link
Contributor

Choose a reason for hiding this comment

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

When newMaxScale < newMinScale I don't think this matches the API docs: https://main-api.flutter.dev/flutter/painting/TextScaler/clamp.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean for the inputs minScaleFactor maxScaleFactor? You are right this implementation removes the assert when maxScaleFactor > minScaleFactor.

I will expend the test and fix it.

But the goal of this newMaxScale <= newMinScale is to check if the new and old bounds overlap. So this condition is required here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed it in a new commit and added more tests. Now it's consistent with the other two clamp functions

@Valansch Valansch force-pushed the fix-textscaler-clamp-assertion branch 2 times, most recently from 5f1de64 to 690f3bd Compare February 19, 2026 09:59
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong left a comment

Choose a reason for hiding this comment

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

LGTM. Thank you for fixing this!

@Valansch Valansch force-pushed the fix-textscaler-clamp-assertion branch from 690f3bd to 93a455f Compare February 19, 2026 22:04
@Valansch
Copy link
Contributor Author

@LongCatIsLooong Thanks for the review. Squashed the commits. Ready to merge from my end.

@victorsanni victorsanni added the autosubmit Merge PR when tree becomes green via auto submit App label Feb 23, 2026
@auto-submit auto-submit bot added this pull request to the merge queue Feb 23, 2026
Merged via the queue into flutter:master with commit cbb7a61 Feb 24, 2026
74 checks passed
@flutter-dashboard flutter-dashboard bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Feb 24, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 24, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 24, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 26, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 26, 2026
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Feb 26, 2026
Roll Flutter from dad6f9d4107a to b31548feb941 (39 revisions)

flutter/flutter@dad6f9d...b31548f

2026-02-25 [email protected] [web] Fix failure on Firefox 148 (flutter/flutter#182855)
2026-02-25 [email protected] Roll Fuchsia Linux SDK from KfPgw04T0OEADLJA5... to XI0Ax7fbtYE4XKYAQ... (flutter/flutter#182887)
2026-02-25 [email protected] Use AnimationStyle curve and reverseCurve in ModalBottomSheet animation (flutter/flutter#181403)
2026-02-25 [email protected] Roll Dart SDK from fd3dce5b6a4e to 5c57e75f1102 (9 revisions) (flutter/flutter#182801)
2026-02-25 98614782+auto-submit[bot]@users.noreply.github.com Reverts "refactor: remove material in context_menu_controller_test, icon_test, list_wheel_scroll_view_test, media_query_test, platform_menu_bar_test (#182697)" (flutter/flutter#182879)
2026-02-25 [email protected] Make sure that an AnimatedSlide doesn't crash in 0x0 environment (flutter/flutter#181535)
2026-02-24 [email protected] Reland Standardize on Test* widgets in *_tester.dart files (flutter/flutter#182632)
2026-02-24 [email protected] docs(Path): clarify that zero-length contours are excluded from computeMetrics (flutter/flutter#180165)
2026-02-24 [email protected] Fix typo in assert message (flutter/flutter#182843)
2026-02-24 [email protected] [win32] Fix overflow in TaskRunnerWindow. (flutter/flutter#182822)
2026-02-24 [email protected] feat: Add --no-uninstall flag to flutter test for integration tests (flutter/flutter#182714)
2026-02-24 [email protected] Rename noFrequencyBasedMinification to useFrequencyBasedMinification (flutter/flutter#182684)
2026-02-24 [email protected] [Impeller] Fix fail to render pixel buffer texture on Linux (flutter/flutter#181656)
2026-02-24 [email protected] Remove FlutterFramework app migration (flutter/flutter#182100)
2026-02-24 [email protected] Roll Packages from 12b43a1 to 062c8d4 (5 revisions) (flutter/flutter#182839)
2026-02-24 [email protected] [web] Run webparagraph tests in CI (flutter/flutter#182092)
2026-02-24 [email protected] Fix a race in EmbedderTest.CanSpecifyCustomUITaskRunner (flutter/flutter#182649)
2026-02-24 [email protected] flutter_tools: Use a super-parameter in several missed cases (flutter/flutter#182581)
2026-02-24 [email protected] Replace more references to `flutter/engine` with `flutter/flutter` (flutter/flutter#182654)
2026-02-24 [email protected] Carousel: Migration from Scrollable+Viewport to CustomScrollView (flutter/flutter#182475)
2026-02-24 [email protected] Refactor impellerc_main to better organize some of its logic (flutter/flutter#182783)
2026-02-24 [email protected] Remove unused `getPluginList ` (flutter/flutter#182660)
2026-02-24 [email protected] Refactor: Remove material from ticker provider test (flutter/flutter#181697)
2026-02-24 [email protected] Roll Skia from 26eebffe12bd to f44d7db68805 (3 revisions) (flutter/flutter#182821)
2026-02-24 [email protected] refactor: remove material in context_menu_controller_test, icon_test, list_wheel_scroll_view_test, media_query_test, platform_menu_bar_test (flutter/flutter#182697)
2026-02-24 [email protected] Roll Skia from 7dad66aae75a to 26eebffe12bd (5 revisions) (flutter/flutter#182810)
2026-02-24 [email protected] Update roadmap for 2026 (flutter/flutter#182798)
2026-02-24 [email protected] Marks Windows tool_tests_commands_1_2 to be unflaky (flutter/flutter#179670)
2026-02-23 [email protected] [web] scroll iOS iframe text input into view (flutter/flutter#179759)
2026-02-23 [email protected] Fix textscaler clamp assertion error (flutter/flutter#181716)
2026-02-23 [email protected] Roll Skia from 9a5a3c92c336 to 7dad66aae75a (4 revisions) (flutter/flutter#182779)
2026-02-23 [email protected] Move more getters from userMessages class to the appropriate places (flutter/flutter#182656)
2026-02-23 [email protected] Manual roll Dart SDK from f8fac50475b8 to fd3dce5b6a4e (6 revisions) (flutter/flutter#182768)
2026-02-23 [email protected] Copy Flutter framework to Add to App FlutterPluginRgistrant (flutter/flutter#182523)
2026-02-23 [email protected] Add progress indicator to artifact downloads (flutter/flutter#181808)
2026-02-23 [email protected] Clarify batch release mode requirements (flutter/flutter#182228)
2026-02-23 [email protected] [web] Remove --disable-gpu from flutter chrome tests (flutter/flutter#182618)
2026-02-23 [email protected] running-apps: update running-apps to use Duration.ago() (flutter/flutter#182172)
2026-02-23 [email protected] Refactor bin/ shell scripts for better performance and safety (flutter/flutter#182674)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

...
ahmedsameha1 pushed a commit to ahmedsameha1/flutter that referenced this pull request Feb 27, 2026
Fix `TextScaler.clamp` assertion failure when clamping already-clamped
scalers

Fixes flutter#174828

## Description

When calling `clamp()` on an already-clamped `TextScaler`, the
`_ClampedTextScaler.clamp()` method was checking if `minScaleFactor ==
maxScaleFactor` **before** calculating the actual new min/max values.
This caused assertion failures (`'maxScale > minScale': is not true`)
when the calculated `newMinScale` and `newMaxScale` became equal after
applying `max()`/`min()` operations.

### Example scenario that triggers the bug:
```dart
final textScaler = MediaQuery.of(context).textScaler;
// First clamp - creates a _ClampedTextScaler
textScaler.clamp(minScaleFactor: 1.0, maxScaleFactor: 1.1);
// Internal framework code calls clamp() again (e.g., in BottomNavigationBar)
// This could result in newMinScale == newMaxScale, triggering the assertion
```

### The fix:
Calculate `newMinScale` and `newMaxScale` **first**, then check for
equality. If they are equal, return a `_LinearTextScaler` instead of
creating a `_ClampedTextScaler` that would fail its constructor
assertion.


### Important
This breaks before undefined behaviour. We now through an assertion
error when new bounds are out of bounds even when min and max are equal.
For more detail view my comment on this
[here](flutter#174828 (comment))

---------

Co-authored-by: Victor Sanni <[email protected]>
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.

textScalar.clamp throws Failed assertion: line 118 pos 80: 'maxScale > minScale': is not true.

3 participants