diff --git a/src/test/ipywidgets/ipywidgetsTheme.unit.test.ts b/src/test/ipywidgets/ipywidgetsTheme.unit.test.ts new file mode 100644 index 00000000000..9dab1983428 --- /dev/null +++ b/src/test/ipywidgets/ipywidgetsTheme.unit.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from '../../platform/vscode-path/path'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants.node'; + +suite('IPyWidgets Theming Bridge', () => { + test('Bridge CSS exists and maps key JupyterLab vars', async () => { + const cssPath = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src/webviews/webview-side/ipywidgets/renderer/jupyterlabThemeBridge.css' + ); + expect(fs.pathExistsSync(cssPath)).to.equal(true, 'Bridge CSS file missing'); + const css = fs.readFileSync(cssPath, 'utf8'); + // Must scope to container to avoid global leaks + expect(css).to.include('.cell-output-ipywidget-background'); + // Font family bridge + expect(css).to.match(/--jp-ui-font-family:\s*var\(--vscode-editor-font-family\)/); + // Foreground mapping + expect(css).to.match(/--jp-content-font-color0:\s*var\(--vscode-editor-foreground\)/); + // Border mapping + expect(css).to.match(/--jp-border-color1:\s*var\(--vscode-panel-border.*\)/); + // Widgets label color mapping + expect(css).to.match(/--jp-widgets-label-color:\s*var\(--vscode-editor-foreground\)/); + // Input color mappings + expect(css).to.match(/--jp-widgets-input-color:\s*var\(--vscode-input-foreground.*\)/); + expect(css).to.match(/--jp-widgets-input-background-color:\s*var\(/); + expect(css).to.match(/--jp-widgets-input-border-color:\s*var\(/); + }); + + test('Renderer imports the bridge CSS', async () => { + const rendererIndex = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src/webviews/webview-side/ipywidgets/renderer/index.ts' + ); + const ts = fs.readFileSync(rendererIndex, 'utf8'); + expect(ts).to.match(/import '\.\/jupyterlabThemeBridge\.css';/); + }); + + test('Kernel imports the bridge CSS to cover kernel-created containers', async () => { + const kernelIndex = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src/webviews/webview-side/ipywidgets/kernel/index.ts' + ); + const ts = fs.readFileSync(kernelIndex, 'utf8'); + expect(ts).to.match(/import '\.\.\/renderer\/jupyterlabThemeBridge\.css';/); + }); + + test('No hardcoded white background for widget container', async () => { + const files = [ + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/webviews/webview-side/ipywidgets/renderer/styles.css'), + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/webviews/webview-side/interactive-common/common.css') + ]; + for (const f of files) { + const css = fs.readFileSync(f, 'utf8'); + const widgetBlockIndex = css.indexOf('.cell-output-ipywidget-background'); + expect(widgetBlockIndex).to.be.greaterThan(-1, `${path.basename(f)} missing widget container style`); + // Ensure we don't force white background; allow transparent or theme var + expect(css).to.not.match(/\.cell-output-ipywidget-background[\s\S]*background:\s*white/i); + } + }); +}); diff --git a/src/webviews/webview-side/interactive-common/common.css b/src/webviews/webview-side/interactive-common/common.css index b9fda79582e..1b5cd90ca51 100644 --- a/src/webviews/webview-side/interactive-common/common.css +++ b/src/webviews/webview-side/interactive-common/common.css @@ -189,7 +189,7 @@ html { } .cell-output-ipywidget-background { - background: white !important; + background: transparent !important; } .cell-output-plot-background * { diff --git a/src/webviews/webview-side/ipywidgets/kernel/index.ts b/src/webviews/webview-side/ipywidgets/kernel/index.ts index aa889ec9ee7..e176a79a950 100644 --- a/src/webviews/webview-side/ipywidgets/kernel/index.ts +++ b/src/webviews/webview-side/ipywidgets/kernel/index.ts @@ -11,6 +11,9 @@ import { ScriptManager } from './scriptManager'; import { IJupyterLabWidgetManagerCtor, INotebookModel } from './types'; import { NotebookMetadata } from '../../../../platform/common/utils'; +// Ensure theme variables are available to any kernel-created containers +import '../renderer/jupyterlabThemeBridge.css'; + class WidgetManagerComponent { private readonly widgetManager: WidgetManager; private readonly scriptManager: ScriptManager; diff --git a/src/webviews/webview-side/ipywidgets/renderer/index.ts b/src/webviews/webview-side/ipywidgets/renderer/index.ts index 3d488ed304e..baa4553a149 100644 --- a/src/webviews/webview-side/ipywidgets/renderer/index.ts +++ b/src/webviews/webview-side/ipywidgets/renderer/index.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import './styles.css'; +import './jupyterlabThemeBridge.css'; import type * as nbformat from '@jupyterlab/nbformat'; import { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer'; import { createDeferred, Deferred } from '../../../../platform/common/utils/async'; diff --git a/src/webviews/webview-side/ipywidgets/renderer/jupyterlabThemeBridge.css b/src/webviews/webview-side/ipywidgets/renderer/jupyterlabThemeBridge.css new file mode 100644 index 00000000000..16181355611 --- /dev/null +++ b/src/webviews/webview-side/ipywidgets/renderer/jupyterlabThemeBridge.css @@ -0,0 +1,91 @@ +/* + Bridge VS Code webview theme variables to JupyterLab CSS variables + ipywidgets reads JupyterLab variables like --jp-layout-color*, --jp-content-font-color*. + We scope these to the ipywidget output container to avoid leaking styles. +*/ + +/* Base scope */ +.cell-output-ipywidget-background { + /* Ensure readable default text */ + color: var(--vscode-editor-foreground); + /* Fonts */ + --jp-ui-font-family: var(--vscode-editor-font-family); + --jp-content-font-family: var(--vscode-editor-font-family); + --jp-code-font-family: var(--vscode-editor-font-family); + --jp-ui-font-size1: var(--vscode-font-size, 13px); + --jp-content-font-size1: var(--vscode-font-size, 14px); + --jp-code-font-size: var(--vscode-editor-font-size, 13px); + + /* Foreground colors */ + --jp-content-font-color0: var(--vscode-editor-foreground); + --jp-content-font-color1: var(--vscode-editor-foreground); + --jp-content-font-color2: var(--vscode-editor-foreground); + --jp-content-font-color3: var(--vscode-editor-foreground); + --jp-ui-font-color0: var(--vscode-editor-foreground); + --jp-ui-font-color1: var(--vscode-editor-foreground); + --jp-ui-font-color2: var(--vscode-editor-foreground); + --jp-ui-font-color3: var(--vscode-editor-foreground); + + /* Inverse font colors are used on brand/success/etc backgrounds */ + --jp-inverse-ui-font-color0: var(--vscode-button-foreground); + --jp-inverse-ui-font-color1: var(--vscode-button-foreground); + --jp-ui-inverse-font-color0: var(--vscode-button-foreground); + --jp-ui-inverse-font-color1: var(--vscode-button-foreground); + + /* Brand colors and focus accents */ + --jp-brand-color0: var(--vscode-button-background); + --jp-brand-color1: var(--vscode-textLink-foreground, var(--vscode-focusBorder)); + --jp-brand-color2: var(--vscode-focusBorder); + + /* Border width baseline */ + --jp-border-width: 1px; + + /* Inputs (text, select, etc.) */ + --jp-widgets-input-color: var(--vscode-input-foreground, var(--vscode-editor-foreground)); + /* Default to notebook cell editor bgcolor; dark theme overrides below */ + --jp-widgets-input-background-color: var(--vscode-notebook-cellEditorBackground, var(--vscode-editor-background)); + --jp-widgets-input-border-color: var(--vscode-input-border, + var(--vscode-panel-border, var(--vscode-editorPane-border))); + --jp-widgets-input-focus-border-color: var(--vscode-focusBorder); + + /* Readout text (e.g., slider value) */ + --jp-widgets-readout-color: var(--vscode-editor-foreground); + + /* Links */ + --jp-content-link-color: var(--vscode-textLink-foreground); + + /* Borders */ + --jp-border-color1: var(--vscode-panel-border, var(--vscode-editorPane-border)); + --jp-border-color2: var(--vscode-panel-border, var(--vscode-editorPane-border)); + + /* Widgets */ + --jp-widgets-color: var(--vscode-editor-foreground); + --jp-widgets-label-color: var(--vscode-editor-foreground); + --jp-widgets-font-size: var(--vscode-editor-font-size, 13px); +} + +/* Light theme specifics */ +.vscode-light .cell-output-ipywidget-background { + /* In light theme, prefer the cell editor background to match notebook cells */ + --jp-layout-color0: var(--vscode-notebook-cellEditorBackground); + --jp-layout-color1: var(--vscode-notebook-cellEditorBackground); + --jp-layout-color2: var(--vscode-notebook-cellEditorBackground); + --jp-layout-color3: var(--vscode-notebook-cellEditorBackground); + --jp-widgets-input-background-color: var(--vscode-notebook-cellEditorBackground); +} + +/* Dark theme specifics */ +.vscode-dark .cell-output-ipywidget-background { + /* In dark theme, use input background which aligns well with controls */ + --jp-layout-color0: var(--vscode-input-background); + --jp-layout-color1: var(--vscode-input-background); + --jp-layout-color2: var(--vscode-input-background); + --jp-layout-color3: var(--vscode-input-background); + --jp-widgets-input-background-color: var(--vscode-input-background); +} + +/* High contrast adjustments */ +.vscode-high-contrast .cell-output-ipywidget-background { + --jp-border-color1: var(--vscode-contrastActiveBorder, var(--vscode-contrastBorder)); + --jp-border-color2: var(--vscode-contrastBorder, var(--vscode-contrastActiveBorder)); +} diff --git a/src/webviews/webview-side/ipywidgets/renderer/styles.css b/src/webviews/webview-side/ipywidgets/renderer/styles.css index a364b43e682..9344b6a0394 100644 --- a/src/webviews/webview-side/ipywidgets/renderer/styles.css +++ b/src/webviews/webview-side/ipywidgets/renderer/styles.css @@ -1,3 +1,4 @@ .cell-output-ipywidget-background { - background: white !important; + /* Let the container inherit theme colors; bridge will set JupyterLab vars */ + background: transparent !important; }