diff --git a/src/layer/annotation/index.ts b/src/layer/annotation/index.ts index 945026ed78..de1e56bba8 100644 --- a/src/layer/annotation/index.ts +++ b/src/layer/annotation/index.ts @@ -39,7 +39,6 @@ import { } from "#src/layer/index.js"; import type { LoadedDataSubsource } from "#src/layer/layer_data_source.js"; import { SegmentationUserLayer } from "#src/layer/segmentation/index.js"; -import { Overlay } from "#src/overlay.js"; import { getWatchableRenderLayerTransform } from "#src/render_coordinate_transform.js"; import { RenderLayerRole } from "#src/renderlayer.js"; import type { SegmentationDisplayState } from "#src/segmentation_display_state/frontend.js"; @@ -743,16 +742,6 @@ function makeShaderCodeWidget(layer: AnnotationUserLayer) { }); } -class ShaderCodeOverlay extends Overlay { - codeWidget: ShaderCodeWidget; - constructor(public layer: AnnotationUserLayer) { - super(); - this.codeWidget = this.registerDisposer(makeShaderCodeWidget(this.layer)); - this.content.appendChild(this.codeWidget.element); - this.codeWidget.textEditor.refresh(); - } -} - class RenderingOptionsTab extends Tab { codeWidget: ShaderCodeWidget; constructor(public layer: AnnotationUserLayer) { @@ -806,13 +795,13 @@ class RenderingOptionsTab extends Tab { element.appendChild( makeShaderCodeWidgetTopRow( this.layer, - this.codeWidget, - ShaderCodeOverlay, + this.codeWidget.element, + makeShaderCodeWidget, { - title: "Documentation on image layer rendering", + title: "Documentation on annotation layer rendering", href: "https://github.com/google/neuroglancer/blob/master/src/annotation/rendering.md", + type: "Annotation", }, - "neuroglancer-annotation-dropdown-shader-top-row", ), ); diff --git a/src/layer/annotation/style.css b/src/layer/annotation/style.css index 137b1a2dd9..1c9238ff8f 100644 --- a/src/layer/annotation/style.css +++ b/src/layer/annotation/style.css @@ -24,12 +24,6 @@ flex-shrink: 0; } -.neuroglancer-annotation-dropdown-shader-top-row { - display: flex; - flex-direction: row; - align-items: center; -} - .neuroglancer-annotation-shader-property-list { max-height: 8em; overflow: auto; diff --git a/src/layer/image/index.ts b/src/layer/image/index.ts index 241b69853e..95a45b8114 100644 --- a/src/layer/image/index.ts +++ b/src/layer/image/index.ts @@ -34,7 +34,6 @@ import { UserLayer, } from "#src/layer/index.js"; import type { LoadedDataSubsource } from "#src/layer/layer_data_source.js"; -import { Overlay } from "#src/overlay.js"; import { getChannelSpace } from "#src/render_coordinate_transform.js"; import { RenderScaleHistogram, @@ -544,13 +543,13 @@ class RenderingOptionsTab extends Tab { element.appendChild( makeShaderCodeWidgetTopRow( this.layer, - this.codeWidget, - ShaderCodeOverlay, + this.codeWidget.element, + makeShaderCodeWidget, { title: "Documentation on image layer rendering", href: "https://github.com/google/neuroglancer/blob/master/src/sliceview/image_layer_rendering.md", + type: "Image", }, - "neuroglancer-image-dropdown-top-row", ), ); element.appendChild( @@ -576,17 +575,6 @@ class RenderingOptionsTab extends Tab { } } -class ShaderCodeOverlay extends Overlay { - codeWidget: ShaderCodeWidget; - constructor(public layer: ImageUserLayer) { - super(); - this.codeWidget = this.registerDisposer(makeShaderCodeWidget(this.layer)); - this.content.classList.add("neuroglancer-image-layer-shader-overlay"); - this.content.appendChild(this.codeWidget.element); - this.codeWidget.textEditor.refresh(); - } -} - registerLayerType(ImageUserLayer); registerVolumeLayerType(VolumeType.IMAGE, ImageUserLayer); // Use ImageUserLayer as a fallback layer type if there is a `volume` subsource. diff --git a/src/layer/image/style.css b/src/layer/image/style.css index 2a90dc0f48..0e7f684e0f 100644 --- a/src/layer/image/style.css +++ b/src/layer/image/style.css @@ -27,17 +27,6 @@ border: 1px solid transparent; } -.neuroglancer-image-dropdown-top-row { - display: flex; - flex-direction: row; - align-items: center; -} - -.neuroglancer-image-layer-shader-overlay .neuroglancer-shader-code-widget { - width: 80vw; - height: 80vh; -} - .neuroglancer-selection-details-value-grid { display: grid; grid-auto-rows: auto; diff --git a/src/layer/segmentation/style.css b/src/layer/segmentation/style.css index dd92be0194..9044d1dd96 100644 --- a/src/layer/segmentation/style.css +++ b/src/layer/segmentation/style.css @@ -24,18 +24,6 @@ height: 6em; } -.neuroglancer-segmentation-dropdown-skeleton-shader-header { - display: flex; - flex-direction: row; - align-items: center; -} - -.neuroglancer-segmentation-layer-skeleton-shader-overlay - .neuroglancer-shader-code-widget { - width: 80vw; - height: 80vh; -} - .neuroglancer-segment-list-entry { display: flex; flex-direction: row; diff --git a/src/layer/single_mesh/index.ts b/src/layer/single_mesh/index.ts index d1e25a03d3..4c5ace025d 100644 --- a/src/layer/single_mesh/index.ts +++ b/src/layer/single_mesh/index.ts @@ -23,7 +23,6 @@ import { UserLayer, } from "#src/layer/index.js"; import type { LoadedDataSubsource } from "#src/layer/layer_data_source.js"; -import { Overlay } from "#src/overlay.js"; import type { VertexAttributeInfo } from "#src/single_mesh/base.js"; import { getShaderAttributeType, @@ -139,7 +138,7 @@ function makeShaderCodeWidget(layer: SingleMeshUserLayer) { }); } -class VertexAttributeWidget extends RefCounted { +export class VertexAttributeWidget extends RefCounted { element = document.createElement("div"); constructor( public attributes: WatchableValueInterface< @@ -215,13 +214,13 @@ class DisplayOptionsTab extends Tab { element.appendChild( makeShaderCodeWidgetTopRow( this.layer, - this.codeWidget, - ShaderCodeOverlay, + this.codeWidget.element, + makeShaderCodeWidget, { - title: "Documentation on image layer rendering", + title: "Documentation on mesh rendering", href: "https://github.com/google/neuroglancer/blob/master/src/sliceview/image_layer_rendering.md", + type: "Mesh", }, - "neuroglancer-single-mesh-dropdown-top-row", ), ); element.appendChild(this.attributeWidget.element); @@ -239,22 +238,6 @@ class DisplayOptionsTab extends Tab { } } -class ShaderCodeOverlay extends Overlay { - attributeWidget: VertexAttributeWidget; - codeWidget: ShaderCodeWidget; - constructor(public layer: SingleMeshUserLayer) { - super(); - this.attributeWidget = this.registerDisposer( - makeVertexAttributeWidget(layer), - ); - this.codeWidget = this.registerDisposer(makeShaderCodeWidget(layer)); - this.content.classList.add("neuroglancer-single-mesh-layer-shader-overlay"); - this.content.appendChild(this.attributeWidget.element); - this.content.appendChild(this.codeWidget.element); - this.codeWidget.textEditor.refresh(); - } -} - registerLayerType(SingleMeshUserLayer); registerLayerTypeDetector((subsource) => { if (subsource.singleMesh !== undefined) { diff --git a/src/layer/single_mesh/style.css b/src/layer/single_mesh/style.css index 5986dc4d47..8366345e1d 100644 --- a/src/layer/single_mesh/style.css +++ b/src/layer/single_mesh/style.css @@ -21,19 +21,6 @@ .neuroglancer-single-mesh-dropdown .neuroglancer-shader-code-widget { height: 6em; - width: 60ch; - border: 1px solid transparent; -} - -.neuroglancer-single-mesh-dropdown-top-row { - display: flex; - flex-direction: row; - align-items: center; -} - -.neuroglancer-single-mesh-shader-overlay .neuroglancer-shader-code-widget { - width: 80vw; - height: 80vh; } .neuroglancer-single-mesh-attribute-widget { @@ -43,11 +30,6 @@ grid-template-columns: [type] auto [name] auto [range] auto; } -.neuroglancer-single-mesh-layer-shader-overlay - .neuroglancer-single-mesh-attribute-widget { - max-height: 20vh; -} - .neuroglancer-single-mesh-attribute { font-family: monospace; display: contents; diff --git a/src/overlay.css b/src/overlay.css index 35d4c06e5e..365010815c 100644 --- a/src/overlay.css +++ b/src/overlay.css @@ -14,17 +14,17 @@ * limitations under the License. */ -.overlay { +.neuroglancer-overlay { height: 100%; width: 100%; position: fixed; - z-index: 99; + z-index: 100; top: 0; left: 0; background-color: rgba(0, 0, 0, 0.8); } -.overlay-content { +.neuroglancer-overlay-content { position: absolute; top: 50%; left: 50%; @@ -32,5 +32,60 @@ background-color: white; z-index: 100; color: black; - padding: 1em; +} + +.neuroglancer-framed-dialog:focus-visible { + outline: none; +} + +.neuroglancer-framed-dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1.5rem; + border-bottom: 1px solid #ccc; + height: 2rem; + margin-bottom: 1px; +} + +.neuroglancer-framed-dialog-body { + display: flex; + padding: 0 1.5rem; + overflow: auto; +} + +.neuroglancer-framed-dialog-title { + margin-right: auto; + font-size: large; + font-weight: bold; + color: #333; +} + +.neuroglancer-framed-dialog-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-top: 1px solid #ccc; + margin-top: 1px; +} + +.neuroglancer-framed-dialog-primary-button { + margin-left: auto; +} + +.neuroglancer-framed-dialog-close-icon { + background-color: transparent; + border: none; + padding: 0; +} + +.neuroglancer-framed-dialog-close-icon svg { + stroke: #141415; + opacity: 0.6; +} + +.neuroglancer-framed-dialog-close-icon:hover svg { + stroke: white; + opacity: 1; } diff --git a/src/overlay.ts b/src/overlay.ts index 8ead82ac04..ff28d89cfa 100644 --- a/src/overlay.ts +++ b/src/overlay.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import svg_close from "ikonate/icons/close.svg?raw"; import { AutomaticallyFocusedElement } from "#src/util/automatic_focus.js"; import { RefCounted } from "#src/util/disposable.js"; import { @@ -21,6 +22,7 @@ import { KeyboardEventBinder, } from "#src/util/keyboard_bindings.js"; import "#src/overlay.css"; +import { makeIcon } from "#src/widget/icon.js"; export const overlayKeyboardHandlerPriority = 100; @@ -39,10 +41,10 @@ export class Overlay extends RefCounted { this.keyMap.addParent(defaultEventMap, Number.NEGATIVE_INFINITY); ++overlaysOpen; const container = (this.container = document.createElement("div")); - container.className = "overlay"; + container.className = "neuroglancer-overlay"; const content = (this.content = document.createElement("div")); this.registerDisposer(new AutomaticallyFocusedElement(content)); - content.className = "overlay-content"; + content.className = "neuroglancer-overlay-content"; container.appendChild(content); document.body.appendChild(container); this.registerDisposer(new KeyboardEventBinder(this.container, this.keyMap)); @@ -62,3 +64,68 @@ export class Overlay extends RefCounted { super.disposed(); } } + +/** + * A dialog that has a header, body, and footer. + * The header contains a title and a close icon. + * The footer contains a primary button. + * The body is where the main content goes. + * @param title - The title text to display in the header + * @param primaryButtonText - The text to display on the primary button + * @param extraClassPrefix - Optional CSS class prefix to add to all dialog elements + * @param onPrimaryButtonClick - Optional callback function to execute when primary button is clicked. Provide null to give no action. + */ + +export class FramedDialog extends Overlay { + header: HTMLDivElement; + headerTitle: HTMLSpanElement; + closeMenuIcon: HTMLElement; + primaryButton: HTMLButtonElement; + body: HTMLDivElement; + footer: HTMLDivElement; + constructor( + title: string = "Dialog", + primaryButtonText: string = "Close", + extraClassPrefix?: string, + onPrimaryButtonClick?: () => void | null, + ) { + super(); + + const header = (this.header = document.createElement("div")); + const closeMenuIcon = (this.closeMenuIcon = makeIcon({ svg: svg_close })); + this.registerEventListener(closeMenuIcon, "click", () => this.close()); + const headerTitle = (this.headerTitle = document.createElement("span")); + headerTitle.textContent = title; + header.appendChild(headerTitle); + header.appendChild(closeMenuIcon); + this.content.appendChild(header); + + const body = (this.body = document.createElement("div")); + this.content.appendChild(body); + + const footer = (this.footer = document.createElement("div")); + const primaryButton = (this.primaryButton = + document.createElement("button")); + primaryButton.textContent = primaryButtonText; + if (onPrimaryButtonClick === undefined) { + this.registerEventListener(primaryButton, "click", () => this.close()); + } else if (onPrimaryButtonClick !== null) { + this.registerEventListener(primaryButton, "click", onPrimaryButtonClick); + } + footer.appendChild(primaryButton); + this.content.appendChild(this.footer); + + const classPrefixes = ["neuroglancer-framed-dialog", extraClassPrefix]; + for (const classPrefix of classPrefixes) { + if (classPrefix !== undefined) { + this.content.classList.add(`${classPrefix}`); + this.header.classList.add(`${classPrefix}-header`); + this.headerTitle.classList.add(`${classPrefix}-title`); + this.closeMenuIcon.classList.add(`${classPrefix}-close-icon`); + this.body.classList.add(`${classPrefix}-body`); + this.footer.classList.add(`${classPrefix}-footer`); + this.primaryButton.classList.add(`${classPrefix}-primary-button`); + } + } + } +} diff --git a/src/ui/screenshot_menu.css b/src/ui/screenshot_menu.css index 98ecb1e831..2c6c7953db 100644 --- a/src/ui/screenshot_menu.css +++ b/src/ui/screenshot_menu.css @@ -34,23 +34,23 @@ outline: 0; } -.neuroglancer-screenshot-dialog { +.neuroglancer-screenshot { width: 48.75rem; - padding: 0; margin: 1.25rem auto; position: relative; transform: none; left: auto; top: auto; - border-radius: 0.5rem; font-family: sans-serif; } -.neuroglancer-screenshot-main-body-container { +.neuroglancer-screenshot-body { height: auto; max-height: calc(100vh - 200px); overflow-y: auto; overflow-x: hidden; + display: block; + padding: 0; } .neuroglancer-screenshot-title { @@ -73,12 +73,8 @@ } /* Div at the top which contains the close */ -.neuroglancer-screenshot-close { - border-bottom: 1px solid var(--gray50); - display: flex; - align-items: center; - padding: 0.75rem 1rem; - gap: 10px; +.neuroglancer-screenshot-header { + padding: 1.5rem 1rem; } /* Filename input menu */ @@ -185,12 +181,6 @@ padding: 0; } -.neuroglancer-screenshot-close-button { - margin-left: auto; - background-color: transparent; - border: 0; -} - /* Screenshot resolution table - voxel resolution, panel resolution */ .neuroglancer-screenshot-size-text { margin: 0.75rem 0 0.75rem 0; @@ -218,8 +208,8 @@ } .neuroglancer-screenshot-resolution-preview-container { - border-top: 1px solid var(--gray50); - border-bottom: 1px solid var(--gray50); + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; background: var(--gray500); width: 100%; padding: 1rem 1rem 0.5rem 1rem; @@ -265,13 +255,19 @@ outline: 0; border: 0; cursor: pointer; - height: 1rem; margin-left: auto; position: relative; - width: 33.33%; justify-content: flex-end; } +.neuroglancer-screenshot-copy-icon:hover svg { + stroke: var(--gray600); +} + +.neuroglancer-screenshot-copy-icon:hover { + background-color: var(--gray300); +} + .neuroglancer-screenshot-dimension { color: var(--gray600); } @@ -320,41 +316,45 @@ } /* Icons in the dialog */ -.neuroglancer-screenshot-dialog .neuroglancer-icon svg { - stroke: rgba(20, 20, 21, 0.4); +.neuroglancer-screenshot-tooltip svg, +.neuroglancer-screenshot-copy-icon svg { + stroke: var(--gray400); width: 1rem; height: 1rem; } -.neuroglancer-screenshot-dialog .neuroglancer-icon { +.neuroglancer-screenshot-tooltip { + padding: 0; width: 1rem; height: 1rem; - min-width: inherit; min-height: inherit; - padding: 0; + cursor: auto; } -.neuroglancer-screenshot-dialog .neuroglancer-icon:hover { +.neuroglancer-screenshot-tooltip:hover { background: none; } +.neuroglancer-screenshot-tooltip:hover svg { + stroke: var(--gray600); +} + /* Footer with progress and buttons */ -.neuroglancer-screenshot-footer-container { +.neuroglancer-screenshot-footer { margin: 0; display: flex; padding: 0.75rem 1rem; - border-top: 1px solid var(--gray50); } .neuroglancer-screenshot-progress-text { margin: 0; flex: 1; font-weight: 590; - cursor: pointer; padding: 0; font-size: 0.813rem; align-self: center; color: var(--primary700); + cursor: wait; } .neuroglancer-screenshot-footer-button { @@ -366,6 +366,11 @@ font-weight: 590; color: var(--gray800); margin: 0 0 0 0.25rem; + cursor: pointer; +} + +.neuroglancer-screenshot-footer-button:hover { + color: var(--gray600); } .neuroglancer-screenshot-footer-button:disabled { diff --git a/src/ui/screenshot_menu.ts b/src/ui/screenshot_menu.ts index 636ca1fe18..1daebefbbe 100644 --- a/src/ui/screenshot_menu.ts +++ b/src/ui/screenshot_menu.ts @@ -17,10 +17,9 @@ */ import "#src/ui/screenshot_menu.css"; -import svg_close from "ikonate/icons/close.svg?raw"; import svg_help from "ikonate/icons/help.svg?raw"; import { throttle } from "lodash-es"; -import { Overlay } from "#src/overlay.js"; +import { FramedDialog } from "#src/overlay.js"; import { StatusMessage } from "#src/status.js"; import { setClipboard } from "#src/util/clipboard.js"; import type { @@ -242,10 +241,8 @@ function parseResolution( * For example, an x2 scale will cause the viewer in slice views to zoom in by a factor of 2 * such that when the number of pixels in the slice view is doubled, the FOV remains the same. */ -export class ScreenshotDialog extends Overlay { +export class ScreenshotDialog extends FramedDialog { private nameInput: HTMLInputElement; - private takeScreenshotButton: HTMLButtonElement; - private closeMenuButton: HTMLButtonElement; private cancelScreenshotButton: HTMLButtonElement; private forceScreenshotButton: HTMLButtonElement; private statisticsTable: HTMLTableElement; @@ -255,7 +252,6 @@ export class ScreenshotDialog extends Overlay { private filenameInputContainer: HTMLDivElement; private screenshotSizeText: HTMLDivElement; private warningElement: HTMLDivElement; - private footerScreenshotActionBtnsContainer: HTMLDivElement; private progressText: HTMLParagraphElement; private scaleRadioButtonsContainer: HTMLDivElement; private keepSliceFOVFixedCheckbox: HTMLInputElement; @@ -281,7 +277,14 @@ export class ScreenshotDialog extends Overlay { private screenshotHeight: number = 0; private screenshotPixelSize: HTMLElement; constructor(private screenshotManager: ScreenshotManager) { - super(); + super( + "Screenshot" /* Header title */, + "Take screenshot" /* Primary button text */, + "neuroglancer-screenshot" /* Extra class prefix */, + () => { + this.screenshot(); + } /* Primary button action */, + ); this.initializeUI(); this.setupEventListeners(); @@ -312,39 +315,30 @@ export class ScreenshotDialog extends Overlay { } private setupHelpTooltips() { - const generalSettingsTooltip = makeIcon({ - svg: svg_help, - title: splitIntoLines(TOOLTIPS.generalSettingsTooltip), - }); - - const orthographicSettingsTooltip = makeIcon({ - svg: svg_help, - title: TOOLTIPS.orthographicSettingsTooltip, - }); - orthographicSettingsTooltip.classList.add( - "neuroglancer-screenshot-resolution-table-tooltip", - ); - - const layerDataTooltip = makeIcon({ - svg: svg_help, - title: splitIntoLines(TOOLTIPS.layerDataTooltip), - }); - layerDataTooltip.classList.add( - "neuroglancer-screenshot-resolution-table-tooltip", + const makeTooltip = (title: string, inTable = false) => { + const tooltip = makeIcon({ + svg: svg_help, + title: splitIntoLines(title), + }); + tooltip.classList.add("neuroglancer-screenshot-tooltip"); + if (inTable) { + tooltip.classList.add( + "neuroglancer-screenshot-resolution-table-tooltip", + ); + } + return tooltip; + }; + const generalSettingsTooltip = makeTooltip(TOOLTIPS.generalSettingsTooltip); + const orthographicSettingsTooltip = makeTooltip( + TOOLTIPS.orthographicSettingsTooltip, + true, ); - - const scaleFactorHelpTooltip = makeIcon({ - svg: svg_help, - title: splitIntoLines(TOOLTIPS.scaleFactorHelpTooltip), - }); - - const panelScaleTooltip = makeIcon({ - svg: svg_help, - title: splitIntoLines(TOOLTIPS.panelScaleTooltip), - }); - panelScaleTooltip.classList.add( - "neuroglancer-screenshot-resolution-table-tooltip", + const layerDataTooltip = makeTooltip(TOOLTIPS.layerDataTooltip, true); + const scaleFactorHelpTooltip = makeTooltip( + TOOLTIPS.scaleFactorHelpTooltip, + true, ); + const panelScaleTooltip = makeTooltip(TOOLTIPS.panelScaleTooltip, true); return (this.helpTooltips = { generalSettingsTooltip, @@ -363,27 +357,13 @@ export class ScreenshotDialog extends Overlay { parentElement.classList.add("neuroglancer-screenshot-overlay"); } - const titleText = document.createElement("h2"); - titleText.classList.add("neuroglancer-screenshot-title"); - titleText.textContent = "Screenshot"; - - this.closeMenuButton = this.createButton( - null, - () => this.close(), - "neuroglancer-screenshot-close-button", - svg_close, - ); - this.cancelScreenshotButton = this.createButton( "Cancel screenshot", () => this.cancelScreenshot(), "neuroglancer-screenshot-footer-button", ); - this.takeScreenshotButton = this.createButton( - "Take screenshot", - () => this.screenshot(), - "neuroglancer-screenshot-footer-button", - ); + const takeScreenshotButton = this.primaryButton; + takeScreenshotButton.classList.add("neuroglancer-screenshot-footer-button"); this.forceScreenshotButton = this.createButton( "Force screenshot", () => this.forceScreenshot(), @@ -407,21 +387,8 @@ export class ScreenshotDialog extends Overlay { this.filenameInputContainer.appendChild(nameInputLabel); this.filenameInputContainer.appendChild(this.createNameInput()); - const closeAndHelpContainer = document.createElement("div"); - closeAndHelpContainer.classList.add("neuroglancer-screenshot-close"); - - closeAndHelpContainer.appendChild(titleText); - closeAndHelpContainer.appendChild(this.closeMenuButton); - - // This is the header - this.content.appendChild(closeAndHelpContainer); - - const mainBody = document.createElement("div"); - mainBody.classList.add("neuroglancer-screenshot-main-body-container"); - this.content.appendChild(mainBody); - - mainBody.appendChild(this.filenameInputContainer); - mainBody.appendChild(this.createScaleRadioButtons()); + this.body.appendChild(this.filenameInputContainer); + this.body.appendChild(this.createScaleRadioButtons()); const previewContainer = document.createElement("div"); previewContainer.classList.add( @@ -477,26 +444,15 @@ export class ScreenshotDialog extends Overlay { settingsPreview.appendChild(this.createPanelResolutionTable()); settingsPreview.appendChild(this.createLayerResolutionTable()); - mainBody.appendChild(previewContainer); - mainBody.appendChild(this.createStatisticsTable()); + this.body.appendChild(previewContainer); + this.body.appendChild(this.createStatisticsTable()); - this.footerScreenshotActionBtnsContainer = document.createElement("div"); - this.footerScreenshotActionBtnsContainer.classList.add( - "neuroglancer-screenshot-footer-container", - ); this.progressText = document.createElement("p"); this.progressText.classList.add("neuroglancer-screenshot-progress-text"); - this.footerScreenshotActionBtnsContainer.appendChild(this.progressText); - this.footerScreenshotActionBtnsContainer.appendChild( - this.cancelScreenshotButton, - ); - this.footerScreenshotActionBtnsContainer.appendChild( - this.takeScreenshotButton, - ); - this.footerScreenshotActionBtnsContainer.appendChild( - this.forceScreenshotButton, - ); - this.content.appendChild(this.footerScreenshotActionBtnsContainer); + this.footer.appendChild(this.progressText); + this.footer.appendChild(this.cancelScreenshotButton); + this.footer.appendChild(takeScreenshotButton); + this.footer.appendChild(this.forceScreenshotButton); this.screenshotManager.previewScreenshot(); this.updateUIBasedOnMode(); @@ -932,7 +888,8 @@ export class ScreenshotDialog extends Overlay { this.keepSliceFOVFixedCheckbox.disabled = false; this.forceScreenshotButton.disabled = true; this.cancelScreenshotButton.disabled = true; - this.takeScreenshotButton.disabled = false; + const takeScreenshotButton = this.primaryButton; + takeScreenshotButton.disabled = false; this.progressText.textContent = ""; this.forceScreenshotButton.title = ""; } else { @@ -945,7 +902,8 @@ export class ScreenshotDialog extends Overlay { this.keepSliceFOVFixedCheckbox.disabled = true; this.forceScreenshotButton.disabled = false; this.cancelScreenshotButton.disabled = false; - this.takeScreenshotButton.disabled = true; + const takeScreenshotButton = this.primaryButton; + takeScreenshotButton.disabled = true; this.progressText.textContent = "Screenshot in progress..."; this.forceScreenshotButton.title = "Force a screenshot of the current view without waiting for all data to be loaded and rendered"; diff --git a/src/ui/segmentation_display_options_tab.ts b/src/ui/segmentation_display_options_tab.ts index 7bb6e9fc80..7120848687 100644 --- a/src/ui/segmentation_display_options_tab.ts +++ b/src/ui/segmentation_display_options_tab.ts @@ -17,7 +17,6 @@ import type { SegmentationUserLayer } from "#src/layer/segmentation/index.js"; import { SKELETON_RENDERING_SHADER_CONTROL_TOOL_ID } from "#src/layer/segmentation/json_keys.js"; import { LAYER_CONTROLS } from "#src/layer/segmentation/layer_controls.js"; -import { Overlay } from "#src/overlay.js"; import { DependentViewWidget } from "#src/widget/dependent_view_widget.js"; import { addLayerControlToOptionsTab } from "#src/widget/layer_control.js"; import { LinkedLayerGroupWidget } from "#src/widget/linked_layer.js"; @@ -80,13 +79,13 @@ export class DisplayOptionsTab extends Tab { parent.appendChild( makeShaderCodeWidgetTopRow( this.layer, - codeWidget, - ShaderCodeOverlay, + codeWidget.element, + makeSkeletonShaderCodeWidget, { - title: "Documentation on image layer rendering", + title: "Documentation on skeleton rendering", href: "https://github.com/google/neuroglancer/blob/master/src/sliceview/image_layer_rendering.md", + type: "Skeleton", }, - "neuroglancer-segmentation-dropdown-skeleton-shader-header", ), ); parent.appendChild(codeWidget.element); @@ -111,18 +110,3 @@ export class DisplayOptionsTab extends Tab { element.appendChild(skeletonControls.element); } } - -class ShaderCodeOverlay extends Overlay { - codeWidget: ShaderCodeWidget; - constructor(public layer: SegmentationUserLayer) { - super(); - this.codeWidget = this.registerDisposer( - makeSkeletonShaderCodeWidget(layer), - ); - this.content.classList.add( - "neuroglancer-segmentation-layer-skeleton-shader-overlay", - ); - this.content.appendChild(this.codeWidget.element); - this.codeWidget.textEditor.refresh(); - } -} diff --git a/src/ui/shader_code_dialog.css b/src/ui/shader_code_dialog.css new file mode 100644 index 0000000000..6a28aac5fa --- /dev/null +++ b/src/ui/shader_code_dialog.css @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Google Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.neuroglancer-shader-code-editor-dialog-body .neuroglancer-shader-code-widget { + width: 80vw; + height: 65vh; +} + +.neuroglancer-shader-code-editor-dialog + .neuroglancer-single-mesh-attribute-widget { + max-height: 12vh; +} diff --git a/src/ui/shader_code_dialog.ts b/src/ui/shader_code_dialog.ts new file mode 100644 index 0000000000..c64b9b6dd1 --- /dev/null +++ b/src/ui/shader_code_dialog.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2025 Google Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import "#src/ui/shader_code_dialog.css"; +import type { UserLayer } from "#src/layer/index.js"; +import type { VertexAttributeWidget } from "#src/layer/single_mesh/index.js"; +import { FramedDialog } from "#src/overlay.js"; +import type { ShaderCodeWidget } from "#src/widget/shader_code_widget.js"; + +export class ShaderCodeEditorDialog extends FramedDialog { + constructor( + public layer: UserLayer, + private makeShaderCodeWidget: (layer: UserLayer) => ShaderCodeWidget, + title: string = "Shader editor", + makeVertexAttributeWidget?: (layer: UserLayer) => VertexAttributeWidget, + ) { + super(title, "Close editor", "neuroglancer-shader-code-editor-dialog"); + + const codeWidget = this.registerDisposer( + this.makeShaderCodeWidget(this.layer), + ); + if (makeVertexAttributeWidget) { + const attributeWidget = this.registerDisposer( + makeVertexAttributeWidget(this.layer), + ); + this.body.appendChild(attributeWidget.element); + } + this.body.appendChild(codeWidget.element); + + codeWidget.textEditor.refresh(); + } +} diff --git a/src/ui/state_editor.css b/src/ui/state_editor.css index c4eda07694..3fef492f96 100644 --- a/src/ui/state_editor.css +++ b/src/ui/state_editor.css @@ -14,11 +14,13 @@ * limitations under the License. */ -.neuroglancer-state-editor { - width: 80%; +.neuroglancer-state-editor .neuroglancer-state-editor-text-editor { + width: 80vw; + height: 65vh; } -.close-button { - position: absolute; - right: 15px; +.neuroglancer-state-editor-save-container { + display: flex; + justify-content: flex-end; + align-items: center; } diff --git a/src/ui/state_editor.ts b/src/ui/state_editor.ts index d22a6aca4f..a6e4cda7e3 100644 --- a/src/ui/state_editor.ts +++ b/src/ui/state_editor.ts @@ -27,48 +27,47 @@ import "codemirror/addon/fold/foldgutter.css"; import "codemirror/addon/lint/lint.css"; import CodeMirror from "codemirror"; import { debounce } from "lodash-es"; -import { Overlay } from "#src/overlay.js"; import "#src/ui/state_editor.css"; - +import { FramedDialog } from "#src/overlay.js"; import { getCachedJson } from "#src/util/trackable.js"; import type { Viewer } from "#src/viewer.js"; const valueUpdateDelay = 100; -export class StateEditorDialog extends Overlay { +export class StateEditorDialog extends FramedDialog { textEditor: CodeMirror.Editor; applyButton: HTMLButtonElement; downloadButton: HTMLButtonElement; - closeButton: HTMLButtonElement; constructor(public viewer: Viewer) { - super(); - - this.content.classList.add("neuroglancer-state-editor"); + super("State editor", "Close", "neuroglancer-state-editor"); - const buttonApply = (this.applyButton = document.createElement("button")); - buttonApply.textContent = "Apply changes"; - this.content.appendChild(buttonApply); - buttonApply.addEventListener("click", () => this.applyChanges()); - buttonApply.disabled = true; + const saveAndCloseWrapper = document.createElement("div"); + saveAndCloseWrapper.classList.add( + "neuroglancer-state-editor-save-container", + ); + const applyButton = (this.applyButton = document.createElement("button")); + applyButton.classList.add("neuroglancer-state-editor-apply-button"); + applyButton.textContent = "Apply changes"; + saveAndCloseWrapper.appendChild(applyButton); + applyButton.addEventListener("click", () => this.applyChanges()); + applyButton.disabled = true; - const buttonClose = (this.closeButton = document.createElement("button")); - buttonClose.classList.add("close-button"); - buttonClose.textContent = "Close"; - this.content.appendChild(buttonClose); - buttonClose.addEventListener("click", () => this.dispose()); + saveAndCloseWrapper.appendChild(this.primaryButton); const downloadButton = (this.downloadButton = document.createElement("button")); downloadButton.textContent = "Download"; downloadButton.title = "Download state as a JSON file"; - this.content.appendChild(downloadButton); downloadButton.addEventListener("click", () => this.downloadState()); + this.footer.appendChild(downloadButton); + this.footer.appendChild(saveAndCloseWrapper); this.textEditor = CodeMirror((_element) => {}, { value: "", mode: { name: "javascript", json: true }, foldGutter: true, gutters: ["CodeMirror-lint-markers", "CodeMirror-foldgutter"], + lineNumbers: true, }); this.updateView(); @@ -76,7 +75,9 @@ export class StateEditorDialog extends Overlay { this.debouncedValueUpdater(); }); - this.content.appendChild(this.textEditor.getWrapperElement()); + const textEditorWrapper = this.textEditor.getWrapperElement(); + textEditorWrapper.classList.add("neuroglancer-state-editor-text-editor"); + this.body.appendChild(textEditorWrapper); this.textEditor.refresh(); } diff --git a/src/widget/shader_code_widget.css b/src/widget/shader_code_widget.css index baa0cb8bc0..66ba3f0c50 100644 --- a/src/widget/shader_code_widget.css +++ b/src/widget/shader_code_widget.css @@ -18,10 +18,12 @@ border: 1px solid red; } -.neuroglancer-shader-code-widget.valid-input { - border: 1px solid green; -} - .neuroglancer-shader-code-widget { border: 1px solid transparent; } + +.neuroglancer-shader-code-widget-top-row { + display: flex; + flex-direction: row; + align-items: center; +} diff --git a/src/widget/shader_code_widget.ts b/src/widget/shader_code_widget.ts index 003bec948a..bbed91c058 100644 --- a/src/widget/shader_code_widget.ts +++ b/src/widget/shader_code_widget.ts @@ -23,13 +23,12 @@ import "codemirror/addon/lint/lint.css"; import { debounce } from "lodash-es"; import type { UserLayer } from "#src/layer/index.js"; -import type { Overlay } from "#src/overlay.js"; +import type { VertexAttributeWidget } from "#src/layer/single_mesh/index.js"; import glslCodeMirror from "#src/third_party/codemirror-glsl.js"; -import { - ElementVisibilityFromTrackableBoolean, - type TrackableBoolean, -} from "#src/trackable_boolean.js"; +import type { TrackableBoolean } from "#src/trackable_boolean.js"; +import { ElementVisibilityFromTrackableBoolean } from "#src/trackable_boolean.js"; import type { WatchableValue } from "#src/trackable_value.js"; +import { ShaderCodeEditorDialog } from "#src/ui/shader_code_dialog.js"; import { RefCounted } from "#src/util/disposable.js"; import { removeFromParent } from "#src/util/dom.js"; import type { WatchableShaderError } from "#src/webgl/dynamic_shader.js"; @@ -54,6 +53,10 @@ glslCodeMirror(CodeMirror); */ const SHADER_UPDATE_DELAY = 500; +type UserLayerWithCodeEditor = UserLayer & { + codeVisible: TrackableBoolean; +}; + interface ShaderCodeState { shaderError: WatchableShaderError; shaderControlState?: ShaderControlState; @@ -82,6 +85,7 @@ export class ShaderCodeWidget extends RefCounted { value: this.state.fragmentMain.value, mode: "glsl", gutters: ["CodeMirror-lint-markers"], + lineNumbers: true, }); this.textEditor.on("change", () => { this.setValidState(undefined); @@ -201,33 +205,31 @@ export class ShaderCodeWidget extends RefCounted { } } -type UserLayerWithCodeEditor = UserLayer & { codeVisible: TrackableBoolean }; -type ShaderCodeOverlayConstructor = new ( - layer: UserLayerWithCodeEditor, -) => T; - -export function makeShaderCodeWidgetTopRow( +export function makeShaderCodeWidgetTopRow( layer: UserLayerWithCodeEditor, - codeWidget: ShaderCodeWidget, - ShaderCodeOverlay: ShaderCodeOverlayConstructor, + codeWidgetElement: HTMLElement, + makeShaderCodeWidget: (layer: UserLayerWithCodeEditor) => ShaderCodeWidget, help: { title: string; href: string; + type: string; }, - className: string, + makeVertexAttributeWidget?: ( + layer: UserLayerWithCodeEditor, + ) => VertexAttributeWidget, ) { const spacer = document.createElement("div"); spacer.style.flex = "1"; const topRow = document.createElement("div"); - topRow.className = className; + topRow.classList.add("neuroglancer-shader-code-widget-top-row"); topRow.appendChild(document.createTextNode("Shader")); topRow.appendChild(spacer); layer.registerDisposer( new ElementVisibilityFromTrackableBoolean( layer.codeVisible, - codeWidget.element, + codeWidgetElement, ), ); @@ -243,7 +245,12 @@ export function makeShaderCodeWidgetTopRow( makeMaximizeButton({ title: "Show larger editor view", onClick: () => { - new ShaderCodeOverlay(layer); + new ShaderCodeEditorDialog( + layer, + makeShaderCodeWidget, + `${help.type} shader editor`, + makeVertexAttributeWidget, + ); }, }), );