Skip to content

Commit 62cbea6

Browse files
committed
Improves custom graph color creation
- moves graph theme from GraphAppState to graph wrapper - reduces JS-based color computing - removes computing colors of CSS custom properties - renames components for clarity
1 parent a1d7c98 commit 62cbea6

File tree

7 files changed

+191
-145
lines changed

7 files changed

+191
-145
lines changed

src/system/color.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,3 +805,11 @@ function _parseHexDigit(charCode: CharCode): number {
805805
}
806806
return 0;
807807
}
808+
809+
export function getCssMixedColorValue(color1: string, color2: string, percent: number): string {
810+
return `color-mix(in srgb, ${color1} ${percent}%, ${color2})`;
811+
}
812+
813+
export function getCssOpacityColorValue(color: string, percent: number): string {
814+
return getCssMixedColorValue(color, 'transparent', percent);
815+
}

src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.react.tsx renamed to src/webviews/apps/plus/graph/graph-wrapper/gl-graph.react.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
CommitType,
3+
CssVariables,
34
ExcludeRefsById,
45
ExternalIconKeys,
56
GetExternalIcon,
@@ -59,7 +60,7 @@ export type GraphWrapperProps = Pick<
5960
| 'rowsStatsLoading'
6061
| 'workingTreeStats'
6162
> &
62-
Pick<GraphAppState, 'activeRow' | 'theming' | 'searchResults' | 'filter'>;
63+
Pick<GraphAppState, 'activeRow' | 'searchResults' | 'filter'> & { theming?: GraphWrapperTheming };
6364

6465
export interface GraphWrapperEvents {
6566
onChangeColumns?: (columns: GraphColumnsConfig) => void;
@@ -197,6 +198,8 @@ interface GraphWrapperAPI {
197198
setRef: (refObject: GraphContainer) => void;
198199
}
199200

201+
export type GraphWrapperTheming = { cssVariables: CssVariables; themeOpacityFactor: number };
202+
200203
export type GraphWrapperSubscriberProps = GraphWrapperProps & GraphWrapperAPI;
201204
export type GraphWrapperInitProps = GraphWrapperProps &
202205
GraphWrapperEvents &
@@ -206,7 +209,7 @@ export type GraphWrapperInitProps = GraphWrapperProps &
206209

207210
const emptyRows: GraphRow[] = [];
208211

209-
export const GraphWrapperReact = memo((initProps: GraphWrapperInitProps) => {
212+
export const GlGraphReact = memo((initProps: GraphWrapperInitProps) => {
210213
const [graph, _graphRef] = useState<GraphContainer | null>(null);
211214
const [context, setContext] = useState(initProps.context);
212215
const [props, setProps] = useState(initProps);

src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper-element.ts renamed to src/webviews/apps/plus/graph/graph-wrapper/gl-graph.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import type {
1212
GraphMissingRefsMetadata,
1313
GraphRefMetadataItem,
1414
} from '../../../../plus/graph/protocol';
15-
import type { GraphWrapperInitProps, GraphWrapperProps, GraphWrapperSubscriberProps } from './graph-wrapper.react';
16-
import { GraphWrapperReact } from './graph-wrapper.react';
15+
import type { GraphWrapperInitProps, GraphWrapperProps, GraphWrapperSubscriberProps } from './gl-graph.react';
16+
import { GlGraphReact } from './gl-graph.react';
1717

1818
/**
1919
* A LitElement web component that encapsulates the GraphWrapperReact component.
@@ -146,7 +146,7 @@ export class GlGraph extends LitElement {
146146

147147
// Mount the React component
148148
this.reactRoot.render(
149-
createElement(GraphWrapperReact, {
149+
createElement(GlGraphReact, {
150150
setRef: this.setRef,
151151
subscriber: this.setReactStateProvider,
152152

src/webviews/apps/plus/graph/graph-wrapper/graph-wrapper.ts

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
/*global document window*/
12
import type GraphContainer from '@gitkraken/gitkraken-components';
2-
import type { GraphRow, GraphZoneType } from '@gitkraken/gitkraken-components';
3+
import type { CssVariables, GraphRow, GraphZoneType } from '@gitkraken/gitkraken-components';
34
import { consume } from '@lit/context';
45
import { SignalWatcher } from '@lit-labs/signals';
56
import { html, LitElement } from 'lit';
6-
import { customElement, query } from 'lit/decorators.js';
7+
import { customElement, query, state } from 'lit/decorators.js';
78
import { ifDefined } from 'lit/directives/if-defined.js';
89
import type { GitGraphRowType } from '../../../../../git/models/graph';
910
import { filterMap } from '../../../../../system/array';
11+
import { getCssMixedColorValue, getCssOpacityColorValue, getCssVariable } from '../../../../../system/color';
12+
import { debounce } from '../../../../../system/function/debounce';
1013
import {
1114
DoubleClickedCommandType,
1215
GetMissingAvatarsCommand,
@@ -20,10 +23,28 @@ import type { CustomEventType } from '../../../shared/components/element';
2023
import { ipcContext } from '../../../shared/contexts/ipc';
2124
import type { TelemetryContext } from '../../../shared/contexts/telemetry';
2225
import { telemetryContext } from '../../../shared/contexts/telemetry';
26+
import type { Disposable } from '../../../shared/events';
27+
import type { ThemeChangeEvent } from '../../../shared/theme';
28+
import { onDidChangeTheme } from '../../../shared/theme';
2329
import { stateContext } from '../context';
2430
import { graphStateContext } from '../stateProvider';
25-
import type { GlGraph } from './graph-wrapper-element';
26-
import './graph-wrapper-element';
31+
import type { GlGraph } from './gl-graph';
32+
import type { GraphWrapperTheming } from './gl-graph.react';
33+
import './gl-graph';
34+
35+
// These properties in the DOM are auto-generated by VS Code from our `contributes.colors` in package.json
36+
const graphLaneThemeColors = new Map([
37+
['--vscode-gitlens-graphLane1Color', '#15a0bf'],
38+
['--vscode-gitlens-graphLane2Color', '#0669f7'],
39+
['--vscode-gitlens-graphLane3Color', '#8e00c2'],
40+
['--vscode-gitlens-graphLane4Color', '#c517b6'],
41+
['--vscode-gitlens-graphLane5Color', '#d90171'],
42+
['--vscode-gitlens-graphLane6Color', '#cd0101'],
43+
['--vscode-gitlens-graphLane7Color', '#f25d2e'],
44+
['--vscode-gitlens-graphLane8Color', '#f2ca33'],
45+
['--vscode-gitlens-graphLane9Color', '#7bd938'],
46+
['--vscode-gitlens-graphLane10Color', '#2ece9d'],
47+
]);
2748

2849
declare global {
2950
// interface HTMLElementTagNameMap {
@@ -57,6 +78,8 @@ export class GlGraphWrapper extends SignalWatcher(LitElement) {
5778
return this;
5879
}
5980

81+
private disposables: Disposable[] = [];
82+
6083
@consume({ context: graphStateContext })
6184
private readonly graphAppState!: typeof graphStateContext.__context__;
6285

@@ -77,6 +100,29 @@ export class GlGraphWrapper extends SignalWatcher(LitElement) {
77100
this.ref = ref;
78101
};
79102

103+
@state()
104+
private theming?: GraphWrapperTheming;
105+
106+
override connectedCallback(): void {
107+
super.connectedCallback?.();
108+
109+
this.theming = this.getGraphTheming();
110+
this.disposables.push(
111+
onDidChangeTheme(
112+
debounce((e: ThemeChangeEvent) => {
113+
this.theming = this.getGraphTheming(e);
114+
}, 100),
115+
),
116+
);
117+
}
118+
119+
override disconnectedCallback(): void {
120+
super.disconnectedCallback?.();
121+
122+
this.disposables.forEach(d => d.dispose());
123+
this.disposables = [];
124+
}
125+
80126
override render() {
81127
const { graphAppState, hostState } = this;
82128

@@ -100,7 +146,7 @@ export class GlGraphWrapper extends SignalWatcher(LitElement) {
100146
.rowsStats=${hostState.rowsStats}
101147
.searchResults=${graphAppState.searchResults}
102148
.selectedRows=${graphAppState.selectedRows}
103-
.theming=${graphAppState.theming}
149+
.theming=${this.theming}
104150
?windowFocused=${hostState.windowFocused}
105151
.workingTreeStats=${hostState.workingTreeStats}
106152
@changecolumns=${this.onColumnsChanged}
@@ -193,4 +239,126 @@ export class GlGraphWrapper extends SignalWatcher(LitElement) {
193239
private onVisibleDaysChanged({ detail }: CustomEventType<'graph-changevisibledays'>) {
194240
this.dispatchEvent(new CustomEvent('gl-graph-change-visible-days', { detail: detail }));
195241
}
242+
243+
private readonly themingDefaults: { cssVariables: CssVariables; themeOpacityFactor: number } = {
244+
cssVariables: (() => {
245+
const bgColor = getCssVariableValue('--color-background');
246+
const mixedGraphColors: CssVariables = {};
247+
let i = 0;
248+
let color;
249+
for (const [colorVar, colorDefault] of graphLaneThemeColors) {
250+
color = getCssVariableValue(colorVar, { fallbackValue: colorDefault });
251+
mixedGraphColors[`--graph-color-${i}`] = color;
252+
for (const mixInt of [15, 25, 45, 50]) {
253+
mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = getCssMixedColorValue(bgColor, color, mixInt);
254+
}
255+
for (const mixInt of [10, 50]) {
256+
mixedGraphColors[`--graph-color-${i}-f${mixInt}`] = getCssOpacityColorValue(color, mixInt);
257+
}
258+
i++;
259+
}
260+
return {
261+
'--app__bg0': getCssVariableValue('--color-background'),
262+
'--panel__bg0': getCssVariableValue('--color-graph-background'),
263+
'--panel__bg1': getCssVariableValue('--color-graph-background2'),
264+
'--section-border': getCssVariableValue('--color-graph-background2'),
265+
'--selected-row': getCssVariableValue('--color-graph-selected-row'),
266+
'--selected-row-border': 'none',
267+
'--hover-row': getCssVariableValue('--color-graph-hover-row'),
268+
'--hover-row-border': 'none',
269+
'--scrollable-scrollbar-thickness': getCssVariableValue('--graph-column-scrollbar-thickness'),
270+
'--scroll-thumb-bg': getCssVariableValue('--vscode-scrollbarSlider-background'),
271+
'--scroll-marker-head-color': getCssVariableValue('--color-graph-scroll-marker-head'),
272+
'--scroll-marker-upstream-color': getCssVariableValue('--color-graph-scroll-marker-upstream'),
273+
'--scroll-marker-highlights-color': getCssVariableValue('--color-graph-scroll-marker-highlights'),
274+
'--scroll-marker-local-branches-color': getCssVariableValue(
275+
'--color-graph-scroll-marker-local-branches',
276+
),
277+
'--scroll-marker-remote-branches-color': getCssVariableValue(
278+
'--color-graph-scroll-marker-remote-branches',
279+
),
280+
'--scroll-marker-stashes-color': getCssVariableValue('--color-graph-scroll-marker-stashes'),
281+
'--scroll-marker-tags-color': getCssVariableValue('--color-graph-scroll-marker-tags'),
282+
'--scroll-marker-selection-color': getCssVariableValue('--color-graph-scroll-marker-selection'),
283+
'--scroll-marker-pull-requests-color': getCssVariableValue('--color-graph-scroll-marker-pull-requests'),
284+
'--stats-added-color': getCssVariableValue('--color-graph-stats-added'),
285+
'--stats-deleted-color': getCssVariableValue('--color-graph-stats-deleted'),
286+
'--stats-files-color': getCssVariableValue('--color-graph-stats-files'),
287+
'--stats-bar-border-radius': getCssVariableValue('--graph-stats-bar-border-radius'),
288+
'--stats-bar-height': getCssVariableValue('--graph-stats-bar-height'),
289+
'--text-selected': getCssVariableValue('--color-graph-text-selected'),
290+
'--text-selected-row': getCssVariableValue('--color-graph-text-selected-row'),
291+
'--text-hovered': getCssVariableValue('--color-graph-text-hovered'),
292+
'--text-dimmed-selected': getCssVariableValue('--color-graph-text-dimmed-selected'),
293+
'--text-dimmed': getCssVariableValue('--color-graph-text-dimmed'),
294+
'--text-normal': getCssVariableValue('--color-graph-text-normal'),
295+
'--text-secondary': getCssVariableValue('--color-graph-text-secondary'),
296+
'--text-disabled': getCssVariableValue('--color-graph-text-disabled'),
297+
'--text-accent': getCssVariableValue('--color-link-foreground'),
298+
'--text-inverse': getCssVariableValue('--vscode-input-background'),
299+
'--text-bright': getCssVariableValue('--vscode-input-background'),
300+
...mixedGraphColors,
301+
};
302+
})(),
303+
themeOpacityFactor: 1,
304+
};
305+
306+
private getGraphTheming(e?: ThemeChangeEvent): GraphWrapperTheming {
307+
// this will be called on theme updated as well as on config updated since it is dependent on the column colors from config changes and the background color from the theme
308+
const computedStyle = e?.computedStyle ?? window.getComputedStyle(document.documentElement);
309+
const bgColor = getCssVariableValue('--color-background', { computedStyle: computedStyle });
310+
311+
const mixedGraphColors: CssVariables = {};
312+
313+
let i = 0;
314+
let color;
315+
for (const [colorVar, colorDefault] of graphLaneThemeColors) {
316+
color = getCssVariableValue(colorVar, { computedStyle: computedStyle, fallbackValue: colorDefault });
317+
318+
mixedGraphColors[`--column-${i}-color`] = getCssVariable(colorVar, computedStyle) || colorDefault;
319+
320+
for (const mixInt of [15, 25, 45, 50]) {
321+
mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = getCssMixedColorValue(bgColor, color, mixInt);
322+
}
323+
324+
i++;
325+
}
326+
327+
const isHighContrastTheme =
328+
e?.isHighContrastTheme ??
329+
(document.body.classList.contains('vscode-high-contrast') ||
330+
document.body.classList.contains('vscode-high-contrast-light'));
331+
332+
return {
333+
cssVariables: {
334+
...this.themingDefaults.cssVariables,
335+
'--selected-row-border': isHighContrastTheme
336+
? `1px solid ${getCssVariableValue('--color-graph-contrast-border', { computedStyle: computedStyle })}`
337+
: 'none',
338+
'--hover-row-border': isHighContrastTheme
339+
? `1px dashed ${getCssVariableValue('--color-graph-contrast-border', { computedStyle: computedStyle })}`
340+
: 'none',
341+
...mixedGraphColors,
342+
},
343+
themeOpacityFactor:
344+
parseInt(getCssVariable('--graph-theme-opacity-factor', computedStyle)) ||
345+
this.themingDefaults.themeOpacityFactor,
346+
};
347+
}
348+
}
349+
350+
function getCssVariableValue(
351+
variable: string,
352+
options?: { computedStyle?: CSSStyleDeclaration; fallbackValue?: string },
353+
): string {
354+
const fallbackValue = options?.computedStyle
355+
? getCssVariable(variable, options?.computedStyle)
356+
: options?.fallbackValue
357+
? options.fallbackValue
358+
: undefined;
359+
360+
if (fallbackValue) {
361+
return `var(${variable}, ${fallbackValue})`;
362+
}
363+
return `var(${variable})`;
196364
}

0 commit comments

Comments
 (0)