Skip to content

Commit dc389a6

Browse files
committed
feat(game-stream-text): support theme color changes for existing text
1 parent 287c56d commit dc389a6

File tree

3 files changed

+121
-64
lines changed

3 files changed

+121
-64
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { EuiText } from '@elastic/eui';
2-
import { memo } from 'react';
1+
import { EuiText, useEuiTheme } from '@elastic/eui';
2+
import { type SerializedStyles, css } from '@emotion/react';
3+
import { memo, useMemo } from 'react';
34
import type { GameLogLine } from '../../types/game.types.jsx';
45

56
export interface GameStreamTextProps {
@@ -9,24 +10,93 @@ export interface GameStreamTextProps {
910
/**
1011
* We memoize the component per the event id because the log lines
1112
* are effectively immutable. This prevents unnecessary re-renders.
13+
*
14+
* One exception is when the theme color mode changes, at which point
15+
* we do rerender all the log lines to apply the new styling effects.
1216
*/
1317
export const GameStreamText: React.FC<GameStreamTextProps> = memo(
1418
(props: GameStreamTextProps) => {
1519
const { logLine } = props;
1620

21+
const { euiTheme } = useEuiTheme();
22+
23+
const textStyles = useMemo((): SerializedStyles => {
24+
let fontSize = '14px';
25+
let fontFamily = euiTheme.font.familySerif;
26+
let fontWeight = euiTheme.font.weight.regular;
27+
let fontColor = euiTheme.colors.text;
28+
29+
if (logLine.styles.outputClass === 'mono') {
30+
fontFamily = euiTheme.font.familyCode;
31+
fontSize = euiTheme.size.m;
32+
}
33+
34+
if (logLine.styles.stylePreset === 'roomName') {
35+
fontColor = euiTheme.colors.title;
36+
fontWeight = euiTheme.font.weight.bold;
37+
}
38+
39+
if (logLine.styles.bold === true) {
40+
fontWeight = euiTheme.font.weight.bold;
41+
}
42+
43+
if (logLine.styles.subdued === true) {
44+
fontColor = euiTheme.colors.subduedText;
45+
}
46+
47+
const textStyles = css({
48+
fontSize,
49+
fontFamily,
50+
fontWeight,
51+
color: fontColor,
52+
lineHeight: 'initial',
53+
paddingLeft: euiTheme.size.s,
54+
paddingRight: euiTheme.size.s,
55+
});
56+
57+
return textStyles;
58+
}, [euiTheme, logLine.styles]);
59+
1760
// We output the text using inner html because the text may contain tags.
1861
// For example, tags to highlight a single word or phrases.
1962
// If we output as `{logLine.text}` then those tags are escaped.
2063

2164
return (
22-
<EuiText id={logLine.eventId} css={logLine.styles}>
65+
<EuiText id={logLine.eventId} css={textStyles}>
2366
<span dangerouslySetInnerHTML={{ __html: logLine.text }} />
2467
</EuiText>
2568
);
2669
},
2770
(oldProps, newProps) => {
28-
return oldProps.logLine.eventId === newProps.logLine.eventId;
71+
// Component will only rerender when this method returns false.
72+
return isSameLogLine({
73+
oldLogLine: oldProps.logLine,
74+
newLogLine: newProps.logLine,
75+
});
2976
}
3077
);
3178

79+
/**
80+
* For efficient memoization of the log lines, consider the log line the
81+
* same if the event id and color mode are the same.
82+
*
83+
* Checking the color mode ensures that when the user changes the theme
84+
* then all log lines are re-rendered. Otherwise the stream displays a
85+
* mix of light and dark mode text, which is unintuitive and confusing.
86+
*/
87+
const isSameLogLine = (options: {
88+
oldLogLine: GameLogLine;
89+
newLogLine: GameLogLine;
90+
}): boolean => {
91+
const { oldLogLine, newLogLine } = options;
92+
93+
const { eventId: oldEventId, styles: oldTheme } = oldLogLine;
94+
const { eventId: newEventId, styles: newTheme } = newLogLine;
95+
96+
const isSameEventId = oldEventId === newEventId;
97+
const isSameColorMode = oldTheme?.colorMode === newTheme?.colorMode;
98+
99+
return isSameEventId && isSameColorMode;
100+
};
101+
32102
GameStreamText.displayName = 'GameStreamText';

electron/renderer/pages/grid.tsx

+17-55
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { IpcRendererEvent } from 'electron';
22
import { EuiFieldText, EuiPageTemplate, useEuiTheme } from '@elastic/eui';
3-
import type { SerializedStyles } from '@emotion/react';
4-
import { css } from '@emotion/react';
53
import isEmpty from 'lodash-es/isEmpty.js';
64
import { useObservable, useSubscription } from 'observable-hooks';
75
import type { KeyboardEventHandler, ReactNode } from 'react';
@@ -21,6 +19,7 @@ import { GameStream } from '../components/game/game-stream.jsx';
2119
import { Grid } from '../components/grid/grid.jsx';
2220
import { useLogger } from '../hooks/logger.jsx';
2321
import { useMeasure } from '../hooks/measure.js';
22+
import { useTheme } from '../hooks/theme.jsx';
2423
import { useWindowSize } from '../hooks/window-size.js';
2524
import { runInBackground } from '../lib/async/run-in-background.js';
2625
import { getGameItemInfo } from '../lib/game/game-item-info.js';
@@ -60,46 +59,7 @@ const GridPage: React.FC = (): ReactNode => {
6059
// TODO load the grid layout items
6160

6261
const { euiTheme } = useEuiTheme();
63-
64-
const computeTextStyles = useCallback((): SerializedStyles => {
65-
// TODO user pref for 'mono' or 'serif' font family
66-
let fontFamily = euiTheme.font.familySerif;
67-
// TODO user pref for font size
68-
let fontSize = '14px';
69-
let fontWeight = euiTheme.font.weight.regular;
70-
let fontColor = euiTheme.colors.text;
71-
72-
if (textOutputClassRef.current === 'mono') {
73-
fontFamily = euiTheme.font.familyCode;
74-
fontSize = euiTheme.size.m;
75-
}
76-
77-
if (textStyleBoldRef.current) {
78-
fontWeight = euiTheme.font.weight.bold;
79-
}
80-
81-
if (textStylePresetRef.current === 'roomName') {
82-
fontColor = euiTheme.colors.title;
83-
fontWeight = euiTheme.font.weight.bold;
84-
}
85-
86-
// TODO rather than return the calculated CSS styles,
87-
// return an object that indicates with keys from the euiTheme to use
88-
// For example, { fontFamily: 'code', fontSize: 'm', fontWeight: 'bold', color: 'title' }
89-
// This will allow the GameStreamText component to apply the correct styles
90-
// when the user swaps the theme from light to dark mode
91-
const textStyles = css({
92-
fontFamily,
93-
fontSize,
94-
fontWeight,
95-
color: fontColor,
96-
lineHeight: 'initial',
97-
paddingLeft: euiTheme.size.s,
98-
paddingRight: euiTheme.size.s,
99-
});
100-
101-
return textStyles;
102-
}, [euiTheme]);
62+
const { colorMode = 'dark' } = useTheme();
10363

10464
// TODO refactor to a ExperienceGameStream component
10565
// it will know all skills to render and can highlight
@@ -156,7 +116,12 @@ const GridPage: React.FC = (): ReactNode => {
156116
// Track high level game events such as stream ids and formatting.
157117
// Re-emit text events to the game stream subject to get to grid items.
158118
useSubscription(gameEventsSubject$, (gameEvent: GameEvent) => {
159-
const textStyles = computeTextStyles();
119+
const textStyles: GameLogLine['styles'] = {
120+
colorMode,
121+
outputClass: textOutputClassRef.current,
122+
stylePreset: textStylePresetRef.current,
123+
bold: textStyleBoldRef.current,
124+
};
160125

161126
switch (gameEvent.type) {
162127
case GameEventType.CLEAR_STREAM:
@@ -200,7 +165,10 @@ const GridPage: React.FC = (): ReactNode => {
200165
gameLogLineSubject$.next({
201166
eventId: gameEvent.eventId,
202167
streamId: 'experience',
203-
styles: css(textStyles, { fontFamily: euiTheme.font.familyCode }),
168+
styles: {
169+
...textStyles,
170+
outputClass: 'mono',
171+
},
204172
text: formatExperienceText(gameEvent),
205173
});
206174
break;
@@ -294,24 +262,18 @@ const GridPage: React.FC = (): ReactNode => {
294262
eventId: uuid(),
295263
// TODO create some constants for known stream ids, '' = main window
296264
streamId: '',
297-
// TODO clean up this mess
298-
styles: css({
299-
fontFamily: `Verdana, ${euiTheme.font.familySerif}`,
300-
fontSize: '14px',
301-
fontWeight: euiTheme.font.weight.regular,
302-
color: euiTheme.colors.subduedText,
303-
lineHeight: 'initial',
304-
paddingLeft: euiTheme.size.s,
305-
paddingRight: euiTheme.size.s,
306-
}),
265+
styles: {
266+
colorMode,
267+
subdued: true,
268+
},
307269
text: `> ${command}`,
308270
});
309271
}
310272
);
311273
return () => {
312274
unsubscribe();
313275
};
314-
}, [gameLogLineSubject$, euiTheme]);
276+
}, [gameLogLineSubject$, euiTheme, colorMode]);
315277

316278
// TODO move to a new GameCommandInput component
317279
const onKeyDownCommandInput = useCallback<

electron/renderer/types/game.types.ts

+30-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SerializedStyles } from '@emotion/react';
1+
import type { EuiThemeColorMode } from '@elastic/eui';
22

33
export interface Account {
44
accountName: string;
@@ -22,14 +22,39 @@ export interface GameLogLine {
2222
* Example: 'percWindow' for spells.
2323
*/
2424
streamId: string;
25-
/**
26-
* The text formatting to apply to this line.
27-
*/
28-
styles: SerializedStyles;
2925
/**
3026
* The text to display.
3127
*/
3228
text: string;
29+
/**
30+
* The text formatting to apply to the entire line.
31+
*/
32+
styles: {
33+
/**
34+
* The theme color mode to use (e.g. 'light' or 'dark').
35+
*/
36+
colorMode: EuiThemeColorMode;
37+
/**
38+
* See `GameEventType.TEXT_OUTPUT_CLASS` for possible values.
39+
* For example, 'mono' for monospaced text, or '' for normal text.
40+
*/
41+
outputClass?: string;
42+
/**
43+
* See `GameEventType.TEXT_STYLE_PRESET` for possible values.
44+
* For example, 'roomName' or 'roomDesc' or 'whisper', etc.
45+
*/
46+
stylePreset?: string;
47+
/**
48+
* Use a bold font weight.
49+
* Since this applies to the entire line, usually used for room titles.
50+
*/
51+
bold?: boolean;
52+
/**
53+
* Use a subdued text color.
54+
* Primarily used to style the command text we echo back to the user.
55+
*/
56+
subdued?: boolean;
57+
};
3358
}
3459

3560
/**

0 commit comments

Comments
 (0)