From ddd0e0746c30ccf0bfd9b32b6225cd2ce3eb240c Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Fri, 22 Mar 2024 03:19:29 +0100 Subject: feat: custom identifier coloring --- subprojects/frontend/package.json | 2 ++ subprojects/frontend/src/editor/EditorArea.tsx | 1 + subprojects/frontend/src/editor/EditorStore.ts | 8 ++++- subprojects/frontend/src/editor/EditorTheme.ts | 36 ++++++++++++++++++++-- .../frontend/src/graph/DotGraphVisualizer.tsx | 8 ++++- subprojects/frontend/src/graph/GraphStore.ts | 36 ++++++++++++++++++++++ subprojects/frontend/src/graph/GraphTheme.tsx | 31 ++++++++++++++++--- subprojects/frontend/src/graph/dotSource.ts | 3 +- .../frontend/src/graph/export/exportDiagram.tsx | 11 ++++++- subprojects/frontend/src/graph/obfuscateColor.ts | 21 +++++++++++++ .../frontend/src/xtext/HighlightingService.ts | 12 ++++++-- 11 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 subprojects/frontend/src/graph/obfuscateColor.ts (limited to 'subprojects/frontend') diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index 73bb463d..970d00a3 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json @@ -55,6 +55,7 @@ "ansi-styles": "^6.2.1", "csstype": "^3.1.3", "d3": "^7.8.5", + "d3-color": "^3.1.0", "d3-graphviz": "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch", "d3-selection": "^3.0.0", "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", @@ -78,6 +79,7 @@ "devDependencies": { "@lezer/generator": "^1.6.0", "@types/d3": "^7.4.3", + "@types/d3-color": "^3.1.3", "@types/d3-graphviz": "^2.6.10", "@types/d3-selection": "^3.0.10", "@types/d3-zoom": "^3.0.8", diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx index aafaad40..ae5cff34 100644 --- a/subprojects/frontend/src/editor/EditorArea.tsx +++ b/subprojects/frontend/src/editor/EditorArea.tsx @@ -39,6 +39,7 @@ export default observer(function EditorArea({ showLineNumbers={editorStore.showLineNumbers} showActiveLine={!editorStore.hasSelection} colorIdentifiers={editorStore.colorIdentifiers} + hexTypeHashes={editorStore.hexTypeHashes} ref={editorParentRef} /> diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 33bca382..f128d70d 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -111,6 +111,8 @@ export default class EditorStore { unsavedChanges = false; + hexTypeHashes: string[] = []; + constructor( initialValue: string, pwaStore: PWAStore, @@ -275,8 +277,12 @@ export default class EditorStore { this.doCommand(nextDiagnostic); } - updateSemanticHighlighting(ranges: IHighlightRange[]): void { + updateSemanticHighlighting( + ranges: IHighlightRange[], + hexTypeHashes: string[], + ): void { this.dispatch(setSemanticHighlighting(ranges)); + this.hexTypeHashes = hexTypeHashes; } updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void { diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 4978c7f7..6deda080 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts @@ -14,6 +14,7 @@ import { type CSSObject, type Theme, } from '@mui/material/styles'; +import { lch } from 'd3-color'; import { range } from 'lodash-es'; import svgURL from '../utils/svgURL'; @@ -21,6 +22,7 @@ import svgURL from '../utils/svgURL'; function createTypeHashStyles( theme: Theme, colorIdentifiers: boolean, + hexTypeHashes: string[], ): CSSObject { if (!colorIdentifiers) { return {}; @@ -34,6 +36,26 @@ function createTypeHashStyles( }, }; }); + hexTypeHashes.forEach((typeHash) => { + let color = lch(`#${typeHash}`); + if (theme.palette.mode === 'dark') { + color = color.brighter(); + if (color.l < 60) { + color.l = 60; + } + } else { + color = color.darker(); + if (color.l > 60) { + color.l = 60; + } + } + result[`.tok-problem-typeHash-_${typeHash}`] = { + '&, .tok-typeName': { + color: color.formatRgb(), + fontWeight: theme.typography.fontWeightEditorTypeHash, + }, + }; + }); return result; } @@ -42,12 +64,20 @@ export default styled('div', { shouldForwardProp: (propName) => propName !== 'showLineNumbers' && propName !== 'showActiveLine' && - propName !== 'colorIdentifiers', + propName !== 'colorIdentifiers' && + propName !== 'hexTypeHashes', })<{ showLineNumbers: boolean; showActiveLine: boolean; colorIdentifiers: boolean; -}>(({ theme, showLineNumbers, showActiveLine, colorIdentifiers }) => { + hexTypeHashes: string[]; +}>(({ + theme, + showLineNumbers, + showActiveLine, + colorIdentifiers, + hexTypeHashes, +}) => { const editorFontStyle: CSSObject = { ...theme.typography.editor, fontWeight: theme.typography.fontWeightEditorNormal, @@ -157,7 +187,7 @@ export default styled('div', { fontStyle: 'normal', }, }, - ...createTypeHashStyles(theme, colorIdentifiers), + ...createTypeHashStyles(theme, colorIdentifiers, hexTypeHashes), }; const matchingStyle: CSSObject = { diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx index cc8b5116..0980ea20 100644 --- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx +++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx @@ -154,7 +154,13 @@ function DotGraphVisualizer({ ], ); - return ; + return ( + + ); } DotGraphVisualizer.defaultProps = { diff --git a/subprojects/frontend/src/graph/GraphStore.ts b/subprojects/frontend/src/graph/GraphStore.ts index d9282326..301b4d86 100644 --- a/subprojects/frontend/src/graph/GraphStore.ts +++ b/subprojects/frontend/src/graph/GraphStore.ts @@ -49,6 +49,8 @@ export function isVisibilityAllowed( return true; } +const TYPE_HASH_HEX_PREFFIX = '_'; + export default class GraphStore { semantics: SemanticsSuccessResult = { nodes: [], @@ -66,6 +68,10 @@ export default class GraphStore { selectedSymbol: RelationMetadata | undefined; + hexTypeHashes: string[] = []; + + private typeHashesMap = new Map(); + constructor( private readonly editorStore: EditorStore, private readonly nameOverride?: string, @@ -188,6 +194,36 @@ export default class GraphStore { this.visibility.delete(key); }); this.setSelectedSymbol(this.selectedSymbol); + this.updateTypeHashes(); + } + + /** + * Maintains a list of past and current color codes to avoid flashing + * when the graph view updates. + * + * As long as the previously used colors are still in in `typeHashesMap`, + * the view will not flash while Graphviz is recomputing, because we'll + * keep emitting styles for the colors. + */ + private updateTypeHashes(): void { + this.semantics.nodes.forEach(({ typeHash }) => { + if ( + typeHash !== undefined && + typeHash.startsWith(TYPE_HASH_HEX_PREFFIX) + ) { + const key = typeHash.substring(TYPE_HASH_HEX_PREFFIX.length); + this.typeHashesMap.set(key, 0); + } + }); + this.hexTypeHashes = Array.from(this.typeHashesMap.keys()); + this.hexTypeHashes.forEach((typeHash) => { + const age = this.typeHashesMap.get(typeHash); + if (age !== undefined && age < 10) { + this.typeHashesMap.set(typeHash, age + 1); + } else { + this.typeHashesMap.delete(typeHash); + } + }); } get colorNodes(): boolean { diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx index 34954345..50a003e0 100644 --- a/subprojects/frontend/src/graph/GraphTheme.tsx +++ b/subprojects/frontend/src/graph/GraphTheme.tsx @@ -13,10 +13,13 @@ import { type CSSObject, type Theme, } from '@mui/material/styles'; +import { lch } from 'd3-color'; import { range } from 'lodash-es'; import svgURL from '../utils/svgURL'; +import obfuscateColor from './obfuscateColor'; + function createEdgeColor( suffix: string, stroke: string, @@ -37,16 +40,32 @@ function createEdgeColor( }; } -function createTypeHashStyles(theme: Theme, colorNodes: boolean): CSSObject { +function createTypeHashStyles( + theme: Theme, + colorNodes: boolean, + typeHashes: string[], +): CSSObject { if (!colorNodes) { return {}; } const result: CSSObject = {}; range(theme.palette.highlight.typeHash.length).forEach((i) => { - result[`.node-typeHash-${i} .node-header`] = { + result[`.node-typeHash-${obfuscateColor(i.toString(10))} .node-header`] = { fill: theme.palette.highlight.typeHash[i]?.box, }; }); + typeHashes.forEach((typeHash) => { + let color = lch(`#${typeHash}`); + if (theme.palette.mode === 'dark') { + color = color.darker(); + if (color.l > 50) { + color.l = 50; + } + } + result[`.node-typeHash-_${obfuscateColor(typeHash)} .node-header`] = { + fill: color.formatRgb(), + }; + }); return result; } @@ -69,10 +88,12 @@ function iconStyle( export function createGraphTheme({ theme, colorNodes, + hexTypeHashes, noEmbedIcons, }: { theme: Theme; colorNodes: boolean; + hexTypeHashes: string[]; noEmbedIcons?: boolean; }): CSSObject { const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24; @@ -111,7 +132,7 @@ export function createGraphTheme({ '.node-exists-UNKNOWN .node-outline': { strokeDasharray: '5 2', }, - ...createTypeHashStyles(theme, colorNodes), + ...createTypeHashStyles(theme, colorNodes, hexTypeHashes), '.edge': { '& text': { fontFamily: theme.typography.fontFamily, @@ -155,7 +176,9 @@ export function createGraphTheme({ export default styled('div', { name: 'GraphTheme', -})<{ colorNodes: boolean }>((args) => ({ + shouldForwardProp: (prop) => + prop !== 'colorNodes' && prop !== 'hexTypeHashes', +})<{ colorNodes: boolean; hexTypeHashes: string[] }>((args) => ({ '& svg': { userSelect: 'none', ...createGraphTheme(args), diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts index 3ac5eb1c..bcd386cf 100644 --- a/subprojects/frontend/src/graph/dotSource.ts +++ b/subprojects/frontend/src/graph/dotSource.ts @@ -10,6 +10,7 @@ import type { } from '../xtext/xtextServiceResults'; import type GraphStore from './GraphStore'; +import obfuscateColor from './obfuscateColor'; const EDGE_WEIGHT = 1; const CONTAINMENT_WEIGHT = 5; @@ -143,7 +144,7 @@ function createNodes( classList.push('node-empty'); } if (node.typeHash !== undefined) { - classList.push(`node-typeHash-${node.typeHash}`); + classList.push(`node-typeHash-${obfuscateColor(node.typeHash)}`); } const classes = classList.join(' '); const name = nodeName(graph, node); diff --git a/subprojects/frontend/src/graph/export/exportDiagram.tsx b/subprojects/frontend/src/graph/export/exportDiagram.tsx index 44489d28..6abbcfdf 100644 --- a/subprojects/frontend/src/graph/export/exportDiagram.tsx +++ b/subprojects/frontend/src/graph/export/exportDiagram.tsx @@ -147,6 +147,7 @@ function appendStyles( svg: SVGSVGElement, theme: Theme, colorNodes: boolean, + hexTypeHashes: string[], fontsCSS: string, ): void { const cache = createCache({ @@ -159,6 +160,7 @@ function appendStyles( const styles = serializeStyles([createGraphTheme], cache.registered, { theme, colorNodes, + hexTypeHashes, noEmbedIcons: true, }); const rules: string[] = [fontsCSS]; @@ -336,7 +338,14 @@ export default async function exportDiagram( } else if (settings.format === 'svg' && settings.embedFonts) { fontsCSS = await fetchFontCSS(); } - appendStyles(svgDocument, copyOfSVG, theme, colorNodes, fontsCSS); + appendStyles( + svgDocument, + copyOfSVG, + theme, + colorNodes, + graph.hexTypeHashes, + fontsCSS, + ); if (settings.format === 'pdf') { const pdf = await serializePDF(copyOfSVG, settings); diff --git a/subprojects/frontend/src/graph/obfuscateColor.ts b/subprojects/frontend/src/graph/obfuscateColor.ts new file mode 100644 index 00000000..57c15804 --- /dev/null +++ b/subprojects/frontend/src/graph/obfuscateColor.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +const regExp = /\d/g; +const offset = 'g'.charCodeAt(0) - '0'.charCodeAt(0); + +/* + * The SVG animation framework we use garbles all numbers while interpolating, + * so we mask numbers in hex color codes by replacing them with letters. + * + * @param color The hex code. + * @return The hex code with no number characters. + */ +export default function obfuscateColor(color: string): string { + return color.replaceAll(regExp, (match) => + String.fromCharCode(match.charCodeAt(0) + offset), + ); +} diff --git a/subprojects/frontend/src/xtext/HighlightingService.ts b/subprojects/frontend/src/xtext/HighlightingService.ts index 447f1401..eacee117 100644 --- a/subprojects/frontend/src/xtext/HighlightingService.ts +++ b/subprojects/frontend/src/xtext/HighlightingService.ts @@ -10,6 +10,8 @@ import type { IHighlightRange } from '../editor/semanticHighlighting'; import type UpdateService from './UpdateService'; import { highlightingResult } from './xtextServiceResults'; +const TYPE_HASH_HEX_PREFIX = 'typeHash-_'; + export default class HighlightingService { constructor( private readonly store: EditorStore, @@ -20,6 +22,7 @@ export default class HighlightingService { const { regions } = highlightingResult.parse(push); const allChanges = this.updateService.computeChangesSinceLastUpdate(); const ranges: IHighlightRange[] = []; + const hexTypeHashes = new Set(); regions.forEach(({ offset, length, styleClasses }) => { if (styleClasses.length === 0) { return; @@ -34,11 +37,16 @@ export default class HighlightingService { to, classes: styleClasses, }); + styleClasses.forEach((styleClass) => { + if (styleClass.startsWith(TYPE_HASH_HEX_PREFIX)) { + hexTypeHashes.add(styleClass.substring(TYPE_HASH_HEX_PREFIX.length)); + } + }); }); - this.store.updateSemanticHighlighting(ranges); + this.store.updateSemanticHighlighting(ranges, Array.from(hexTypeHashes)); } onDisconnect(): void { - this.store.updateSemanticHighlighting([]); + this.store.updateSemanticHighlighting([], []); } } -- cgit v1.2.3-54-g00ecf