Skip to content

Commit c3d30f4

Browse files
authored
fix: masking semi-transparent widgets (#2472)
* fix: masking semi-transparent widgets * chore: update changelog * fix * cleanup * fix
1 parent 54e972d commit c3d30f4

File tree

4 files changed

+188
-24
lines changed

4 files changed

+188
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
});
2929
```
3030

31+
### Fixes
32+
33+
- Masking semi-transparent widgets ([#2472](https://github.com/getsentry/sentry-dart/pull/2472))
34+
3135
## 8.11.0-beta.2
3236

3337
### Features

flutter/lib/src/screenshot/recorder.dart

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'dart:ui';
33

44
import 'package:flutter/rendering.dart';
55
import 'package:flutter/widgets.dart' as widgets;
6+
import 'package:flutter/material.dart' as material;
7+
import 'package:flutter/cupertino.dart' as cupertino;
68
import 'package:meta/meta.dart';
79

810
import '../../sentry_flutter.dart';
@@ -79,10 +81,13 @@ class ScreenshotRecorder {
7981

8082
final filter = _widgetFilter;
8183
if (filter != null) {
84+
final colorScheme = context.findColorScheme();
8285
filter.obscure(
83-
context,
84-
pixelRatio,
85-
Rect.fromLTWH(0, 0, srcWidth * pixelRatio, srcHeight * pixelRatio),
86+
context: context,
87+
pixelRatio: pixelRatio,
88+
colorScheme: colorScheme,
89+
bounds: Rect.fromLTWH(
90+
0, 0, srcWidth * pixelRatio, srcHeight * pixelRatio),
8691
);
8792
}
8893

@@ -137,3 +142,63 @@ class ScreenshotRecorder {
137142
}
138143
}
139144
}
145+
146+
extension on widgets.BuildContext {
147+
WidgetFilterColorScheme findColorScheme() {
148+
WidgetFilterColorScheme? result;
149+
visitAncestorElements((el) {
150+
result = getElementColorScheme(el);
151+
return result == null;
152+
});
153+
154+
if (result == null) {
155+
int limit = 20;
156+
visitor(widgets.Element el) {
157+
// Don't take too much time trying to find the theme.
158+
if (limit-- < 0) {
159+
return;
160+
}
161+
162+
result ??= getElementColorScheme(el);
163+
if (result == null) {
164+
el.visitChildren(visitor);
165+
}
166+
}
167+
168+
visitChildElements(visitor);
169+
}
170+
171+
assert(material.Colors.white.isOpaque);
172+
assert(material.Colors.black.isOpaque);
173+
result ??= const WidgetFilterColorScheme(
174+
background: material.Colors.white,
175+
defaultMask: material.Colors.black,
176+
defaultTextMask: material.Colors.black,
177+
);
178+
179+
return result!;
180+
}
181+
182+
WidgetFilterColorScheme? getElementColorScheme(widgets.Element el) {
183+
final widget = el.widget;
184+
if (widget is material.MaterialApp || widget is material.Scaffold) {
185+
final colorScheme = material.Theme.of(el).colorScheme;
186+
return WidgetFilterColorScheme(
187+
background: colorScheme.surface.asOpaque(),
188+
defaultMask: colorScheme.primary.asOpaque(),
189+
defaultTextMask: colorScheme.primary.asOpaque(),
190+
);
191+
} else if (widget is cupertino.CupertinoApp) {
192+
final colorScheme = cupertino.CupertinoTheme.of(el);
193+
final textColor = colorScheme.textTheme.textStyle.foreground?.color ??
194+
colorScheme.textTheme.textStyle.color ??
195+
colorScheme.primaryColor;
196+
return WidgetFilterColorScheme(
197+
background: colorScheme.scaffoldBackgroundColor.asOpaque(),
198+
defaultMask: colorScheme.primaryColor.asOpaque(),
199+
defaultTextMask: textColor.asOpaque(),
200+
);
201+
}
202+
return null;
203+
}
204+
}

flutter/lib/src/screenshot/widget_filter.dart

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import 'package:flutter/material.dart';
12
import 'package:flutter/rendering.dart';
2-
import 'package:flutter/widgets.dart';
33
import 'package:meta/meta.dart';
44

55
import '../../sentry_flutter.dart';
@@ -11,7 +11,7 @@ class WidgetFilter {
1111
final items = <WidgetFilterItem>[];
1212
final SentryLogger logger;
1313
final SentryMaskingConfig config;
14-
static const _defaultColor = Color.fromARGB(255, 0, 0, 0);
14+
late WidgetFilterColorScheme _scheme;
1515
late double _pixelRatio;
1616
late Rect _bounds;
1717
final _warnedWidgets = <int>{};
@@ -22,9 +22,18 @@ class WidgetFilter {
2222

2323
WidgetFilter(this.config, this.logger);
2424

25-
void obscure(BuildContext context, double pixelRatio, Rect bounds) {
25+
void obscure({
26+
required BuildContext context,
27+
required double pixelRatio,
28+
required Rect bounds,
29+
required WidgetFilterColorScheme colorScheme,
30+
}) {
2631
_pixelRatio = pixelRatio;
2732
_bounds = bounds;
33+
_scheme = colorScheme;
34+
assert(colorScheme.background.isOpaque);
35+
assert(colorScheme.defaultMask.isOpaque);
36+
assert(colorScheme.defaultTextMask.isOpaque);
2837
items.clear();
2938
if (context is Element) {
3039
_process(context);
@@ -81,7 +90,7 @@ class WidgetFilter {
8190
stackTrace: stackTrace);
8291
}
8392
if (parent == null) {
84-
return WidgetFilterItem(_defaultColor, _bounds);
93+
return WidgetFilterItem(_scheme.defaultMask, _bounds);
8594
}
8695
element = parent;
8796
widget = element.widget;
@@ -126,11 +135,23 @@ class WidgetFilter {
126135

127136
Color? color;
128137
if (widget is Text) {
129-
color = (widget).style?.color;
138+
color = widget.style?.color;
139+
if (color == null && renderBox is RenderParagraph) {
140+
color = renderBox.text.style?.color;
141+
}
142+
color ??= _scheme.defaultTextMask;
130143
} else if (widget is EditableText) {
131-
color = (widget).style.color;
144+
color = widget.style.color ?? _scheme.defaultTextMask;
132145
} else if (widget is Image) {
133-
color = (widget).color;
146+
color = widget.color;
147+
}
148+
149+
// We need to make the color non-transparent or the mask would
150+
// also be partially transparent.
151+
if (color == null) {
152+
color = _scheme.defaultMask;
153+
} else if (!color.isOpaque) {
154+
color = Color.alphaBlend(color, _scheme.background);
134155
}
135156

136157
// test-only code
@@ -142,7 +163,8 @@ class WidgetFilter {
142163
return true;
143164
}());
144165

145-
return WidgetFilterItem(color ?? _defaultColor, rect);
166+
assert(color.isOpaque, 'Mask color must be opaque: $color');
167+
return WidgetFilterItem(color, rect);
146168
}
147169

148170
// We cut off some widgets early because they're not visible at all.
@@ -207,3 +229,24 @@ extension on Element {
207229
return result;
208230
}
209231
}
232+
233+
@internal
234+
extension Opaqueness on Color {
235+
@pragma('vm:prefer-inline')
236+
bool get isOpaque => alpha == 0xff;
237+
238+
@pragma('vm:prefer-inline')
239+
Color asOpaque() => isOpaque ? this : Color.fromARGB(0xff, red, green, blue);
240+
}
241+
242+
@internal
243+
class WidgetFilterColorScheme {
244+
final Color defaultMask;
245+
final Color defaultTextMask;
246+
final Color background;
247+
248+
const WidgetFilterColorScheme(
249+
{required this.defaultMask,
250+
required this.defaultTextMask,
251+
required this.background});
252+
}

flutter/test/screenshot/widget_filter_test.dart

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import 'package:flutter/material.dart';
12
import 'package:flutter/services.dart';
2-
import 'package:flutter/widgets.dart';
33
import 'package:flutter_test/flutter_test.dart';
44
import 'package:sentry_flutter/sentry_flutter.dart';
55
import 'package:sentry_flutter/src/screenshot/widget_filter.dart';
@@ -14,6 +14,10 @@ void main() async {
1414
const defaultBounds = Rect.fromLTRB(0, 0, 1000, 1000);
1515
final rootBundle = TestAssetBundle();
1616
final otherBundle = TestAssetBundle();
17+
final colorScheme = WidgetFilterColorScheme(
18+
defaultMask: Colors.white,
19+
defaultTextMask: Colors.green,
20+
background: Colors.red);
1721

1822
final createSut = ({bool redactImages = false, bool redactText = false}) {
1923
final replayOptions = SentryPrivacyOptions();
@@ -30,29 +34,45 @@ void main() async {
3034
testWidgets('redacts the correct number of elements', (tester) async {
3135
final sut = createSut(redactText: true);
3236
final element = await pumpTestElement(tester);
33-
sut.obscure(element, 1.0, defaultBounds);
37+
sut.obscure(
38+
context: element,
39+
pixelRatio: 1.0,
40+
bounds: defaultBounds,
41+
colorScheme: colorScheme);
3442
expect(sut.items.length, 5);
3543
});
3644

3745
testWidgets('does not redact text when disabled', (tester) async {
3846
final sut = createSut(redactText: false);
3947
final element = await pumpTestElement(tester);
40-
sut.obscure(element, 1.0, defaultBounds);
48+
sut.obscure(
49+
context: element,
50+
pixelRatio: 1.0,
51+
bounds: defaultBounds,
52+
colorScheme: colorScheme);
4153
expect(sut.items.length, 0);
4254
});
4355

4456
testWidgets('does not redact elements that are outside the screen',
4557
(tester) async {
4658
final sut = createSut(redactText: true);
4759
final element = await pumpTestElement(tester);
48-
sut.obscure(element, 1.0, Rect.fromLTRB(0, 0, 100, 100));
60+
sut.obscure(
61+
context: element,
62+
pixelRatio: 1.0,
63+
bounds: Rect.fromLTRB(0, 0, 100, 100),
64+
colorScheme: colorScheme);
4965
expect(sut.items.length, 1);
5066
});
5167

5268
testWidgets('correctly determines sizes', (tester) async {
5369
final sut = createSut(redactText: true);
5470
final element = await pumpTestElement(tester);
55-
sut.obscure(element, 1.0, defaultBounds);
71+
sut.obscure(
72+
context: element,
73+
pixelRatio: 1.0,
74+
bounds: defaultBounds,
75+
colorScheme: colorScheme);
5676
expect(sut.items.length, 5);
5777
expect(boundsRect(sut.items[0]), '624x48');
5878
expect(boundsRect(sut.items[1]), '169x20');
@@ -66,7 +86,11 @@ void main() async {
6686
testWidgets('redacts the correct number of elements', (tester) async {
6787
final sut = createSut(redactImages: true);
6888
final element = await pumpTestElement(tester);
69-
sut.obscure(element, 1.0, defaultBounds);
89+
sut.obscure(
90+
context: element,
91+
pixelRatio: 1.0,
92+
bounds: defaultBounds,
93+
colorScheme: colorScheme);
7094
expect(sut.items.length, 3);
7195
});
7296

@@ -106,22 +130,34 @@ void main() async {
106130
testWidgets('does not redact text when disabled', (tester) async {
107131
final sut = createSut(redactImages: false);
108132
final element = await pumpTestElement(tester);
109-
sut.obscure(element, 1.0, defaultBounds);
133+
sut.obscure(
134+
context: element,
135+
pixelRatio: 1.0,
136+
bounds: defaultBounds,
137+
colorScheme: colorScheme);
110138
expect(sut.items.length, 0);
111139
});
112140

113141
testWidgets('does not redact elements that are outside the screen',
114142
(tester) async {
115143
final sut = createSut(redactImages: true);
116144
final element = await pumpTestElement(tester);
117-
sut.obscure(element, 1.0, Rect.fromLTRB(0, 0, 500, 100));
145+
sut.obscure(
146+
context: element,
147+
pixelRatio: 1.0,
148+
bounds: Rect.fromLTRB(0, 0, 500, 100),
149+
colorScheme: colorScheme);
118150
expect(sut.items.length, 1);
119151
});
120152

121153
testWidgets('correctly determines sizes', (tester) async {
122154
final sut = createSut(redactImages: true);
123155
final element = await pumpTestElement(tester);
124-
sut.obscure(element, 1.0, defaultBounds);
156+
sut.obscure(
157+
context: element,
158+
pixelRatio: 1.0,
159+
bounds: defaultBounds,
160+
colorScheme: colorScheme);
125161
expect(sut.items.length, 3);
126162
expect(boundsRect(sut.items[0]), '1x1');
127163
expect(boundsRect(sut.items[1]), '1x1');
@@ -134,7 +170,11 @@ void main() async {
134170
final element = await pumpTestElement(tester, children: [
135171
SentryMask(Padding(padding: EdgeInsets.all(100), child: Text('foo'))),
136172
]);
137-
sut.obscure(element, 1.0, defaultBounds);
173+
sut.obscure(
174+
context: element,
175+
pixelRatio: 1.0,
176+
bounds: defaultBounds,
177+
colorScheme: colorScheme);
138178
expect(sut.items.length, 1);
139179
expect(boundsRect(sut.items[0]), '344x248');
140180
});
@@ -146,7 +186,11 @@ void main() async {
146186
SentryUnmask(newImage()),
147187
SentryUnmask(SentryMask(Text('foo'))),
148188
]);
149-
sut.obscure(element, 1.0, defaultBounds);
189+
sut.obscure(
190+
context: element,
191+
pixelRatio: 1.0,
192+
bounds: defaultBounds,
193+
colorScheme: colorScheme);
150194
expect(sut.items, isEmpty);
151195
});
152196

@@ -155,11 +199,19 @@ void main() async {
155199
final element = await pumpTestElement(tester, children: [
156200
Padding(padding: EdgeInsets.all(100), child: Text('foo')),
157201
]);
158-
sut.obscure(element, 1.0, defaultBounds);
202+
sut.obscure(
203+
context: element,
204+
pixelRatio: 1.0,
205+
bounds: defaultBounds,
206+
colorScheme: colorScheme);
159207
expect(sut.items.length, 1);
160208
expect(boundsRect(sut.items[0]), '144x48');
161209
sut.throwInObscure = true;
162-
sut.obscure(element, 1.0, defaultBounds);
210+
sut.obscure(
211+
context: element,
212+
pixelRatio: 1.0,
213+
bounds: defaultBounds,
214+
colorScheme: colorScheme);
163215
expect(sut.items.length, 1);
164216
expect(boundsRect(sut.items[0]), '344x248');
165217
});

0 commit comments

Comments
 (0)