From 206767161c01f18e9aeac53dca66e138017c1fdc Mon Sep 17 00:00:00 2001 From: Maik Wild Date: Fri, 30 Jan 2026 11:43:02 +0200 Subject: [PATCH 1/2] Introduce TextScaler test to reproduce #174828 --- .../test/painting/text_scaler_test.dart | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/flutter/test/painting/text_scaler_test.dart b/packages/flutter/test/painting/text_scaler_test.dart index 8d9a1046bda35..fdea871d6dde4 100644 --- a/packages/flutter/test/painting/text_scaler_test.dart +++ b/packages/flutter/test/painting/text_scaler_test.dart @@ -57,5 +57,77 @@ void main() { expect(scaler1, scaler3); expect(scaler1, isNot(scaler2)); }); + + testWidgets('Reclamping', (WidgetTester tester) async { + final TextScaler defaultScaler = MediaQueryData.fromView(tester.view).textScaler; + + // Does not raise maxScale > minScale + final TextScaler scaler1 = defaultScaler.clamp(minScaleFactor: 1, maxScaleFactor: 1.5); + final TextScaler scaler2 = scaler1.clamp(minScaleFactor: 0.5, maxScaleFactor: 1); + expect(scaler2, TextScaler.noScaling); + + // No overlap, first scaler < second scaler. uses new minScale of last clamp + final TextScaler scaler3 = defaultScaler.clamp(minScaleFactor: 1, maxScaleFactor: 2); + final TextScaler scaler4 = scaler3.clamp(minScaleFactor: 3, maxScaleFactor: 4); + expect(scaler4, const TextScaler.linear(3)); + + // No overlap, first scaler > second scaler. uses new maxScale of last clamp + final TextScaler scaler5 = defaultScaler.clamp(minScaleFactor: 5, maxScaleFactor: 6); + final TextScaler scaler6 = scaler5.clamp(minScaleFactor: 3, maxScaleFactor: 4); + expect(scaler6, const TextScaler.linear(4)); + + // Overlap, combine clamps + final TextScaler scaler7 = defaultScaler.clamp(minScaleFactor: 1, maxScaleFactor: 3); + final TextScaler scaler8 = scaler7.clamp(minScaleFactor: 2, maxScaleFactor: 4); + expect(scaler8, defaultScaler.clamp(minScaleFactor: 2, maxScaleFactor: 3)); + }); + }); + + testWidgets('ClampedScaler asserts', (WidgetTester tester) async { + final TextScaler defaultScaler = MediaQueryData.fromView(tester.view).textScaler; + final TextScaler clampedScaler = defaultScaler.clamp(minScaleFactor: 1, maxScaleFactor: 5); + expect( + () => clampedScaler.clamp(minScaleFactor: 3, maxScaleFactor: 2), + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'message', + contains('maxScaleFactor >= minScaleFactor'), + ), + ), + ); + + expect( + () => clampedScaler.clamp(minScaleFactor: 3, maxScaleFactor: double.nan), + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'message', + contains('maxScaleFactor >= minScaleFactor'), + ), + ), + ); + + expect( + () => clampedScaler.clamp(minScaleFactor: -1, maxScaleFactor: 2), + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'message', + contains('minScaleFactor >= 0'), + ), + ), + ); + + expect( + () => clampedScaler.clamp(minScaleFactor: double.infinity), + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'message', + contains('minScaleFactor.isFinite'), + ), + ), + ); }); } From 93a455f039fe1cdbd0130bb8304f94129100b954 Mon Sep 17 00:00:00 2001 From: Maik Wild Date: Thu, 19 Feb 2026 23:03:22 +0100 Subject: [PATCH 2/2] ClampedTextScaler: Fix error when applying multiple clamps This fixes #174828 --- .../flutter/lib/src/painting/text_scaler.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/painting/text_scaler.dart b/packages/flutter/lib/src/painting/text_scaler.dart index 6b537c753eb41..8cfc00b18f6f1 100644 --- a/packages/flutter/lib/src/painting/text_scaler.dart +++ b/packages/flutter/lib/src/painting/text_scaler.dart @@ -132,9 +132,19 @@ final class _ClampedTextScaler implements TextScaler { @override TextScaler clamp({double minScaleFactor = 0, double maxScaleFactor = double.infinity}) { - return minScaleFactor == maxScaleFactor - ? _LinearTextScaler(minScaleFactor) - : _ClampedTextScaler(scaler, max(minScaleFactor, minScale), min(maxScaleFactor, maxScale)); + assert(maxScaleFactor >= minScaleFactor); + assert(!maxScaleFactor.isNaN); + assert(minScaleFactor.isFinite); + assert(minScaleFactor >= 0); + + final double newMinScale = max(minScale, minScaleFactor); + final double newMaxScale = min(maxScale, maxScaleFactor); + + if (newMaxScale <= newMinScale) { + // Ranges don't overlap or collapse to a single point. + return TextScaler.linear(clampDouble(minScale, minScaleFactor, maxScaleFactor)); + } + return _ClampedTextScaler(scaler, newMinScale, newMaxScale); } @override