Skip to content

Commit 6b4745d

Browse files
authored
Force canvas redraw when system theme changes (#5861)
1 parent 27701da commit 6b4745d

File tree

10 files changed

+155
-0
lines changed

10 files changed

+155
-0
lines changed

src/components/shared/chart/Canvas.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,18 @@ export class ChartCanvas<Item> extends React.Component<
375375
}
376376
}
377377

378+
override componentDidMount() {
379+
window.addEventListener('profiler-theme-change', this._onThemeChange);
380+
}
381+
382+
override componentWillUnmount() {
383+
window.removeEventListener('profiler-theme-change', this._onThemeChange);
384+
}
385+
386+
_onThemeChange = () => {
387+
this._scheduleDraw();
388+
};
389+
378390
override componentDidUpdate(prevProps: Props<Item>, prevState: State<Item>) {
379391
if (prevProps !== this.props) {
380392
if (

src/components/shared/thread/ActivityGraphCanvas.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,20 @@ export class ActivityGraphCanvas extends React.PureComponent<CanvasProps> {
105105

106106
override componentDidMount() {
107107
this._renderCanvas();
108+
window.addEventListener('profiler-theme-change', this._onThemeChange);
108109
}
109110

111+
override componentWillUnmount() {
112+
window.removeEventListener('profiler-theme-change', this._onThemeChange);
113+
}
114+
115+
_onThemeChange = () => {
116+
// Invalidate the cached category draw styles,
117+
// so they are recreated with the new theme colors.
118+
this._categoryDrawStyles = null;
119+
this._renderCanvas();
120+
};
121+
110122
override componentDidUpdate() {
111123
this._renderCanvas();
112124
}

src/components/shared/thread/SampleGraph.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,17 @@ class ThreadSampleGraphCanvas extends React.PureComponent<CanvasProps> {
119119

120120
override componentDidMount() {
121121
this._renderCanvas();
122+
window.addEventListener('profiler-theme-change', this._onThemeChange);
122123
}
123124

125+
override componentWillUnmount() {
126+
window.removeEventListener('profiler-theme-change', this._onThemeChange);
127+
}
128+
129+
_onThemeChange = () => {
130+
this._renderCanvas();
131+
};
132+
124133
override componentDidUpdate() {
125134
this._renderCanvas();
126135
}

src/components/timeline/Markers.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,17 @@ class TimelineMarkersCanvas extends React.PureComponent<CanvasProps> {
237237

238238
override componentDidMount() {
239239
this._scheduleDraw();
240+
window.addEventListener('profiler-theme-change', this._onThemeChange);
240241
}
241242

243+
override componentWillUnmount() {
244+
window.removeEventListener('profiler-theme-change', this._onThemeChange);
245+
}
246+
247+
_onThemeChange = () => {
248+
this._scheduleDraw();
249+
};
250+
242251
override componentDidUpdate() {
243252
this._scheduleDraw();
244253
}

src/test/components/FlameGraph.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ describe('FlameGraph', function () {
6666
expect(drawCalls).toMatchSnapshot();
6767
});
6868

69+
it('redraws when the system theme changes', () => {
70+
setupFlameGraph();
71+
// Flush the initial draw calls.
72+
flushDrawLog();
73+
74+
// Simulate a theme change.
75+
window.dispatchEvent(new CustomEvent('profiler-theme-change'));
76+
77+
// drawCanvasAfterRaf={false} means the redraw is synchronous, so new draw
78+
// calls should be available immediately without flushing rAF.
79+
const drawCalls = flushDrawLog();
80+
expect(drawCalls.length).toBeGreaterThan(0);
81+
});
82+
6983
it('ignores invertCallstack and always displays non-inverted', () => {
7084
const { getState, dispatch } = setupFlameGraph();
7185
expect(getInvertCallstack(getState())).toBe(false);

src/test/components/SampleGraph.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,21 @@ describe('SampleGraph', function () {
149149
};
150150
}
151151

152+
it('redraws when the system theme changes', () => {
153+
const { getContextDrawCalls } = setup();
154+
155+
// Flush the initial draw calls.
156+
getContextDrawCalls();
157+
158+
// Simulate a theme change.
159+
window.dispatchEvent(new CustomEvent('profiler-theme-change'));
160+
161+
const drawCalls = getContextDrawCalls();
162+
expect(drawCalls.some(([operation]) => operation === 'fillRect')).toBe(
163+
true
164+
);
165+
});
166+
152167
it('matches the component snapshot', () => {
153168
const { sampleGraphCanvas } = setup();
154169
expect(sampleGraphCanvas).toMatchSnapshot();

src/test/components/ThreadActivityGraph.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,22 @@ describe('ThreadActivityGraph', function () {
170170
);
171171
});
172172

173+
it('redraws when the system theme changes', () => {
174+
const { getContextDrawCalls } = setup();
175+
176+
// Flush out any existing draw calls.
177+
getContextDrawCalls();
178+
expect(getContextDrawCalls().length).toEqual(0);
179+
180+
// Simulate a theme change.
181+
window.dispatchEvent(new CustomEvent('profiler-theme-change'));
182+
183+
const drawCalls = getContextDrawCalls();
184+
expect(drawCalls.some(([operation]) => operation === 'beginPath')).toBe(
185+
true
186+
);
187+
});
188+
173189
it('matches the 2d canvas draw snapshot with CPU values', () => {
174190
const profile = getSamplesProfile();
175191
profile.meta.interval = 1;

src/test/components/TimelineMarkers.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ describe('TimelineMarkers', function () {
176176
beforeEach(addRootOverlayElement);
177177
afterEach(removeRootOverlayElement);
178178

179+
it('redraws when the system theme changes', () => {
180+
const { flushRafCalls } = setupWithMarkers(
181+
{ rangeStart: 0, rangeEnd: 10 },
182+
[['DOMEvent', 0, 10]]
183+
);
184+
185+
// Flush the initial draw calls.
186+
flushRafCalls();
187+
flushDrawLog();
188+
189+
// Simulate a theme change.
190+
window.dispatchEvent(new CustomEvent('profiler-theme-change'));
191+
192+
// _scheduleDraw() uses RAF, so flush it before checking.
193+
flushRafCalls();
194+
const drawCalls = flushDrawLog();
195+
expect(drawCalls.length).toBeGreaterThan(0);
196+
});
197+
179198
it('renders correctly overview markers', () => {
180199
window.devicePixelRatio = 1;
181200

src/test/unit/dark-mode.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,50 @@ describe('isDarkMode', function () {
4646
});
4747
});
4848

49+
describe('profiler-theme-change event', function () {
50+
it('is dispatched when the theme changes', function () {
51+
resetForTest();
52+
// Initialize in light mode.
53+
isDarkMode();
54+
55+
const listener = jest.fn();
56+
window.addEventListener('profiler-theme-change', listener);
57+
58+
// Switch to dark via a storage event.
59+
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => 'dark');
60+
window.dispatchEvent(new StorageEvent('storage', { key: 'theme' }));
61+
62+
expect(listener).toHaveBeenCalledTimes(1);
63+
window.removeEventListener('profiler-theme-change', listener);
64+
});
65+
66+
it('is not dispatched during initialization', function () {
67+
resetForTest();
68+
69+
const listener = jest.fn();
70+
window.addEventListener('profiler-theme-change', listener);
71+
72+
isDarkMode(); // triggers setup
73+
74+
expect(listener).not.toHaveBeenCalled();
75+
window.removeEventListener('profiler-theme-change', listener);
76+
});
77+
78+
it('is not dispatched when the theme stays the same', function () {
79+
resetForTest();
80+
isDarkMode(); // initialize as light
81+
82+
const listener = jest.fn();
83+
window.addEventListener('profiler-theme-change', listener);
84+
85+
// Storage event fires but the resolved theme is still light.
86+
window.dispatchEvent(new StorageEvent('storage', { key: 'theme' }));
87+
88+
expect(listener).not.toHaveBeenCalled();
89+
window.removeEventListener('profiler-theme-change', listener);
90+
});
91+
});
92+
4993
describe('initTheme', function () {
5094
it('sets the document element class', function () {
5195
resetForTest();

src/utils/dark-mode.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,18 @@ function _applyTheme(): void {
4040
shouldBeDark = getSystemTheme() === 'dark';
4141
}
4242

43+
const changed = _isDarkModeSetup && _isDarkMode !== shouldBeDark;
4344
_isDarkMode = shouldBeDark;
4445

4546
if (shouldBeDark) {
4647
document.documentElement.classList.add('dark-mode');
4748
} else {
4849
document.documentElement.classList.remove('dark-mode');
4950
}
51+
52+
if (changed) {
53+
window.dispatchEvent(new CustomEvent('profiler-theme-change'));
54+
}
5055
}
5156

5257
export function setThemePreference(pref: ThemePreference): void {

0 commit comments

Comments
 (0)