Skip to content

Commit 49632fc

Browse files
authored
Provide parameter to Icon and IconThemeData for they to consider the context's text scaler (#135708)
Provide a parameter `applyTextScaling` to both `Icon` and `IconDataTheme`. When `true`, the context's `TextScaler` will apply it's `scale` method to the icon size. Fixes #115466
1 parent 4498175 commit 49632fc

9 files changed

Lines changed: 178 additions & 19 deletions

File tree

packages/flutter/lib/src/cupertino/icon_theme_data.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class CupertinoIconThemeData extends IconThemeData with Diagnosticable {
1919
super.color,
2020
super.opacity,
2121
super.shadows,
22+
super.applyTextScaling,
2223
});
2324

2425
/// Called by [IconTheme.of] to resolve [color] against the given [BuildContext].
@@ -40,6 +41,7 @@ class CupertinoIconThemeData extends IconThemeData with Diagnosticable {
4041
Color? color,
4142
double? opacity,
4243
List<Shadow>? shadows,
44+
bool? applyTextScaling,
4345
}) {
4446
return CupertinoIconThemeData(
4547
size: size ?? this.size,
@@ -50,6 +52,7 @@ class CupertinoIconThemeData extends IconThemeData with Diagnosticable {
5052
color: color ?? this.color,
5153
opacity: opacity ?? this.opacity,
5254
shadows: shadows ?? this.shadows,
55+
applyTextScaling: applyTextScaling ?? this.applyTextScaling,
5356
);
5457
}
5558

packages/flutter/lib/src/painting/text_painter.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import 'text_span.dart';
2727

2828
export 'package:flutter/services.dart' show TextRange, TextSelection;
2929

30-
// The default font size if none is specified. This should be kept in
31-
// sync with the default values in text_style.dart, as well as the
32-
// defaults set in the engine (eg, LibTxt's text_style.h, paragraph_style.h).
33-
const double _kDefaultFontSize = 14.0;
30+
/// The default font size if none is specified.
31+
///
32+
/// This should be kept in sync with the defaults set in the engine (e.g.,
33+
/// LibTxt's text_style.h, paragraph_style.h).
34+
const double kDefaultFontSize = 14.0;
3435

3536
/// How overflowing text should be handled.
3637
///
@@ -971,7 +972,7 @@ class TextPainter {
971972
// Use the default font size to multiply by as RichText does not
972973
// perform inheriting [TextStyle]s and would otherwise
973974
// fail to apply textScaler.
974-
fontSize: textScaler.scale(_kDefaultFontSize),
975+
fontSize: textScaler.scale(kDefaultFontSize),
975976
maxLines: maxLines,
976977
textHeightBehavior: _textHeightBehavior,
977978
ellipsis: ellipsis,

packages/flutter/lib/src/painting/text_style.dart

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ const String _kColorForegroundWarning = 'Cannot provide both a color and a foreg
2929
const String _kColorBackgroundWarning = 'Cannot provide both a backgroundColor and a background\n'
3030
'The backgroundColor argument is just a shorthand for "background: Paint()..color = color".';
3131

32-
// The default font size if none is specified. This should be kept in
33-
// sync with the default values in text_painter.dart, as well as the
34-
// defaults set in the engine (eg, LibTxt's text_style.h, paragraph_style.h).
35-
const double _kDefaultFontSize = 14.0;
36-
3732
// Examples can assume:
3833
// late BuildContext context;
3934

@@ -1353,7 +1348,7 @@ class TextStyle with Diagnosticable {
13531348
fontWeight: fontWeight ?? this.fontWeight,
13541349
fontStyle: fontStyle ?? this.fontStyle,
13551350
fontFamily: fontFamily ?? this.fontFamily,
1356-
fontSize: textScaler.scale(fontSize ?? this.fontSize ?? _kDefaultFontSize),
1351+
fontSize: textScaler.scale(fontSize ?? this.fontSize ?? kDefaultFontSize),
13571352
height: height ?? this.height,
13581353
textHeightBehavior: effectiveTextHeightBehavior,
13591354
strutStyle: strutStyle == null ? null : ui.StrutStyle(

packages/flutter/lib/src/widgets/icon.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'framework.dart';
1313
import 'icon_data.dart';
1414
import 'icon_theme.dart';
1515
import 'icon_theme_data.dart';
16+
import 'media_query.dart';
1617

1718
/// A graphical icon widget drawn with a glyph from a font described in
1819
/// an [IconData] such as material's predefined [IconData]s in [Icons].
@@ -80,6 +81,7 @@ class Icon extends StatelessWidget {
8081
this.shadows,
8182
this.semanticLabel,
8283
this.textDirection,
84+
this.applyTextScaling,
8385
}) : assert(fill == null || (0.0 <= fill && fill <= 1.0)),
8486
assert(weight == null || (0.0 < weight)),
8587
assert(opticalSize == null || (0.0 < opticalSize));
@@ -231,14 +233,28 @@ class Icon extends StatelessWidget {
231233
/// specified, either directly using this property or using [Directionality].
232234
final TextDirection? textDirection;
233235

236+
/// Whether to scale the size of this widget using the ambient [MediaQuery]'s [TextScaler].
237+
///
238+
/// This is specially useful when you have an icon associated with a text, as
239+
/// scaling the text without scaling the icon would result in a confusing
240+
/// interface.
241+
///
242+
/// Defaults to the nearest [IconTheme]'s
243+
/// [IconThemeData.applyTextScaling].
244+
final bool? applyTextScaling;
245+
234246
@override
235247
Widget build(BuildContext context) {
236248
assert(this.textDirection != null || debugCheckHasDirectionality(context));
237249
final TextDirection textDirection = this.textDirection ?? Directionality.of(context);
238250

239251
final IconThemeData iconTheme = IconTheme.of(context);
240252

241-
final double? iconSize = size ?? iconTheme.size;
253+
final bool applyTextScaling = this.applyTextScaling ?? iconTheme.applyTextScaling ?? false;
254+
255+
final double tentativeIconSize = size ?? iconTheme.size ?? kDefaultFontSize;
256+
257+
final double iconSize = applyTextScaling ? MediaQuery.textScalerOf(context).scale(tentativeIconSize) : tentativeIconSize;
242258

243259
final double? iconFill = fill ?? iconTheme.fill;
244260

@@ -332,5 +348,6 @@ class Icon extends StatelessWidget {
332348
properties.add(IterableProperty<Shadow>('shadows', shadows, defaultValue: null));
333349
properties.add(StringProperty('semanticLabel', semanticLabel, defaultValue: null));
334350
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
351+
properties.add(DiagnosticsProperty<bool>('applyTextScaling', applyTextScaling, defaultValue: null));
335352
}
336353
}

packages/flutter/lib/src/widgets/icon_theme.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class IconTheme extends InheritedTheme {
7676
color: iconThemeData.color ?? const IconThemeData.fallback().color,
7777
opacity: iconThemeData.opacity ?? const IconThemeData.fallback().opacity,
7878
shadows: iconThemeData.shadows ?? const IconThemeData.fallback().shadows,
79+
applyTextScaling: iconThemeData.applyTextScaling ?? const IconThemeData.fallback().applyTextScaling,
7980
);
8081
}
8182

packages/flutter/lib/src/widgets/icon_theme_data.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class IconThemeData with Diagnosticable {
3131
this.color,
3232
double? opacity,
3333
this.shadows,
34+
this.applyTextScaling,
3435
}) : _opacity = opacity,
3536
assert(fill == null || (0.0 <= fill && fill <= 1.0)),
3637
assert(weight == null || (0.0 < weight)),
@@ -48,7 +49,8 @@ class IconThemeData with Diagnosticable {
4849
opticalSize = 48.0,
4950
color = const Color(0xFF000000),
5051
_opacity = 1.0,
51-
shadows = null;
52+
shadows = null,
53+
applyTextScaling = false;
5254

5355
/// Creates a copy of this icon theme but with the given fields replaced with
5456
/// the new values.
@@ -61,6 +63,7 @@ class IconThemeData with Diagnosticable {
6163
Color? color,
6264
double? opacity,
6365
List<Shadow>? shadows,
66+
bool? applyTextScaling,
6467
}) {
6568
return IconThemeData(
6669
size: size ?? this.size,
@@ -71,6 +74,7 @@ class IconThemeData with Diagnosticable {
7174
color: color ?? this.color,
7275
opacity: opacity ?? this.opacity,
7376
shadows: shadows ?? this.shadows,
77+
applyTextScaling: applyTextScaling ?? this.applyTextScaling,
7478
);
7579
}
7680

@@ -90,6 +94,7 @@ class IconThemeData with Diagnosticable {
9094
color: other.color,
9195
opacity: other.opacity,
9296
shadows: other.shadows,
97+
applyTextScaling: other.applyTextScaling,
9398
);
9499
}
95100

@@ -118,7 +123,8 @@ class IconThemeData with Diagnosticable {
118123
&& grade != null
119124
&& opticalSize != null
120125
&& color != null
121-
&& opacity != null;
126+
&& opacity != null
127+
&& applyTextScaling != null;
122128

123129
/// The default for [Icon.size].
124130
///
@@ -163,6 +169,9 @@ class IconThemeData with Diagnosticable {
163169
/// The default for [Icon.shadows].
164170
final List<Shadow>? shadows;
165171

172+
/// The default for [Icon.applyTextScaling].
173+
final bool? applyTextScaling;
174+
166175
/// Linearly interpolate between two icon theme data objects.
167176
///
168177
/// {@macro dart.ui.shadow.lerp}
@@ -179,6 +188,7 @@ class IconThemeData with Diagnosticable {
179188
color: Color.lerp(a?.color, b?.color, t),
180189
opacity: ui.lerpDouble(a?.opacity, b?.opacity, t),
181190
shadows: Shadow.lerpList(a?.shadows, b?.shadows, t),
191+
applyTextScaling: t < 0.5 ? a?.applyTextScaling : b?.applyTextScaling,
182192
);
183193
}
184194

@@ -195,7 +205,8 @@ class IconThemeData with Diagnosticable {
195205
&& other.opticalSize == opticalSize
196206
&& other.color == color
197207
&& other.opacity == opacity
198-
&& listEquals(other.shadows, shadows);
208+
&& listEquals(other.shadows, shadows)
209+
&& other.applyTextScaling == applyTextScaling;
199210
}
200211

201212
@override
@@ -208,6 +219,7 @@ class IconThemeData with Diagnosticable {
208219
color,
209220
opacity,
210221
shadows == null ? null : Object.hashAll(shadows!),
222+
applyTextScaling,
211223
);
212224

213225
@override
@@ -221,5 +233,6 @@ class IconThemeData with Diagnosticable {
221233
properties.add(ColorProperty('color', color, defaultValue: null));
222234
properties.add(DoubleProperty('opacity', opacity, defaultValue: null));
223235
properties.add(IterableProperty<Shadow>('shadows', shadows, defaultValue: null));
236+
properties.add(DiagnosticsProperty<bool>('applyTextScaling', applyTextScaling, defaultValue: null));
224237
}
225238
}

packages/flutter/lib/src/widgets/widget_span.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import 'package:flutter/rendering.dart';
1010
import 'basic.dart';
1111
import 'framework.dart';
1212

13-
const double _kEngineDefaultFontSize = 14.0;
14-
1513
// Examples can assume:
1614
// late WidgetSpan myWidgetSpan;
1715

@@ -100,7 +98,7 @@ class WidgetSpan extends PlaceholderSpan {
10098
final List<Widget> widgets = <Widget>[];
10199
// _kEngineDefaultFontSize is the default font size to use when none of the
102100
// ancestor spans specifies one.
103-
final List<double> fontSizeStack = <double>[_kEngineDefaultFontSize];
101+
final List<double> fontSizeStack = <double>[kDefaultFontSize];
104102
int index = 0;
105103
// This assumes an InlineSpan tree's logical order is equivalent to preorder.
106104
bool visitSubtree(InlineSpan span) {

packages/flutter/test/cupertino/icon_theme_data_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ void main() {
1515
grade: 0.0,
1616
opticalSize: 48.0,
1717
color: Color(0xAAAAAAAA),
18-
opacity: 0.5
18+
opacity: 0.5,
19+
applyTextScaling: true,
1920
);
2021

2122
late IconThemeData retrieved;

packages/flutter/test/widgets/icon_test.dart

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,126 @@ void main() {
113113
expect(renderObject.size, equals(const Size.square(24.0)));
114114
});
115115

116+
testWidgetsWithLeakTracking('Icon sizing - no theme, default size, considering text scaler', (WidgetTester tester) async {
117+
await tester.pumpWidget(
118+
const MediaQuery(
119+
data: MediaQueryData(
120+
textScaler: _TextDoubler(),
121+
),
122+
child: Directionality(
123+
textDirection: TextDirection.ltr,
124+
child: Center(
125+
child: Icon(
126+
null,
127+
applyTextScaling: true,
128+
),
129+
),
130+
),
131+
),
132+
);
133+
134+
final RenderBox renderObject = tester.renderObject(find.byType(Icon));
135+
expect(renderObject.size, equals(const Size.square(48.0)));
136+
});
137+
138+
testWidgetsWithLeakTracking('Icon sizing - no theme, explicit size, considering text scaler', (WidgetTester tester) async {
139+
await tester.pumpWidget(
140+
const MediaQuery(
141+
data: MediaQueryData(
142+
textScaler: _TextDoubler(),
143+
),
144+
child: Directionality(
145+
textDirection: TextDirection.ltr,
146+
child: Center(
147+
child: Icon(
148+
null,
149+
size: 96.0,
150+
applyTextScaling: true,
151+
),
152+
),
153+
),
154+
),
155+
);
156+
157+
final RenderBox renderObject = tester.renderObject(find.byType(Icon));
158+
expect(renderObject.size, equals(const Size.square(192.0)));
159+
});
160+
161+
testWidgetsWithLeakTracking('Icon sizing - sized theme, considering text scaler', (WidgetTester tester) async {
162+
await tester.pumpWidget(
163+
const MediaQuery(
164+
data: MediaQueryData(
165+
textScaler: _TextDoubler(),
166+
),
167+
child: Directionality(
168+
textDirection: TextDirection.ltr,
169+
child: Center(
170+
child: IconTheme(
171+
data: IconThemeData(
172+
size: 36.0,
173+
applyTextScaling: true,
174+
),
175+
child: Icon(null),
176+
),
177+
),
178+
),
179+
),
180+
);
181+
182+
final RenderBox renderObject = tester.renderObject(find.byType(Icon));
183+
expect(renderObject.size, equals(const Size.square(72.0)));
184+
});
185+
186+
testWidgetsWithLeakTracking('Icon sizing - sized theme, explicit size, considering text scale', (WidgetTester tester) async {
187+
await tester.pumpWidget(
188+
const MediaQuery(
189+
data: MediaQueryData(
190+
textScaler: _TextDoubler(),
191+
),
192+
child: Directionality(
193+
textDirection: TextDirection.ltr,
194+
child: Center(
195+
child: IconTheme(
196+
data: IconThemeData(
197+
size: 36.0,
198+
applyTextScaling: true,
199+
),
200+
child: Icon(
201+
null,
202+
size: 48.0,
203+
applyTextScaling: false,
204+
),
205+
),
206+
),
207+
),
208+
),
209+
);
210+
211+
final RenderBox renderObject = tester.renderObject(find.byType(Icon));
212+
expect(renderObject.size, equals(const Size.square(48.0)));
213+
});
214+
215+
testWidgetsWithLeakTracking('Icon sizing - sizeless theme, default size, default consideration for text scaler', (WidgetTester tester) async {
216+
await tester.pumpWidget(
217+
const MediaQuery(
218+
data: MediaQueryData(
219+
textScaler: _TextDoubler(),
220+
),
221+
child: Directionality(
222+
textDirection: TextDirection.ltr,
223+
child: Center(
224+
child: IconTheme(
225+
data: IconThemeData(),
226+
child: Icon(null),
227+
),
228+
),
229+
),
230+
),
231+
);
232+
233+
final RenderBox renderObject = tester.renderObject(find.byType(Icon));
234+
expect(renderObject.size, equals(const Size.square(24.0)));
235+
});
116236

117237
testWidgetsWithLeakTracking('Icon with custom font', (WidgetTester tester) async {
118238
await tester.pumpWidget(
@@ -335,3 +455,13 @@ void main() {
335455
expect(() => Icon(Icons.abc, opticalSize: 0), throwsAssertionError);
336456
});
337457
}
458+
459+
final class _TextDoubler extends TextScaler {
460+
const _TextDoubler();
461+
462+
@override
463+
double scale(double fontSize) => fontSize * 2.0;
464+
465+
@override
466+
double get textScaleFactor => 2.0;
467+
}

0 commit comments

Comments
 (0)