diff --git a/src/main/index.ts b/src/main/index.ts
index 305b2c0..4a19b06 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -1895,6 +1895,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro
autoHideMenuBar: true,
title: `Prism ${SCOPE_LABELS[kind]}`,
alwaysOnTop: getWindowStateStore().getPopoutAlwaysOnTop(kind),
+ ...(process.platform === 'darwin' ? { acceptFirstMouse: true } : {}),
show: false,
...getStaticWindowIconOptions(),
webPreferences: {
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index 5d7b567..66ab258 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -20,6 +20,7 @@ import { getRendererWindowCapabilities } from './windowCapabilities'
export default function App(): JSX.Element {
const [toolbarVisible, setToolbarVisible] = useState(false)
+ const [measurementActive, setMeasurementActive] = useState(false)
const [settingsPanelHeight, setSettingsPanelHeight] = useState(0)
const [bottomBarHeight, setBottomBarHeight] = useState(0)
const [trayReady, setTrayReady] = useState(false)
@@ -302,13 +303,13 @@ export default function App(): JSX.Element {
onMouseUp={useNativeDragRegions ? undefined : handleAltDragEnd}
>
-
+
diff --git a/src/renderer/components/ScopeModule.tsx b/src/renderer/components/ScopeModule.tsx
index b16bacd..7001397 100644
--- a/src/renderer/components/ScopeModule.tsx
+++ b/src/renderer/components/ScopeModule.tsx
@@ -55,6 +55,7 @@ interface ScopeModuleProps {
theme?: ScopeModuleTheme
settings?: ScopeSettings[ScopeKind]
frameScheduler?: FrameScheduler
+ onMeasurementActiveChange?: (active: boolean) => void
dataSource?:
| SpectrumAnalyzerDataSource
| OscilloscopeDataSource
@@ -400,6 +401,7 @@ export default function ScopeModule({
theme,
settings,
frameScheduler,
+ onMeasurementActiveChange,
dataSource,
}: ScopeModuleProps): JSX.Element {
const containerRef = useRef(null)
@@ -447,6 +449,7 @@ export default function ScopeModule({
? visualizer as ScopeMeasurementSource
: null
},
+ onActiveChange: onMeasurementActiveChange,
})
useEffect(() => {
diff --git a/src/renderer/components/Strip.tsx b/src/renderer/components/Strip.tsx
index 0c00b45..5650435 100644
--- a/src/renderer/components/Strip.tsx
+++ b/src/renderer/components/Strip.tsx
@@ -9,7 +9,11 @@ import { usePerformanceStore } from '../stores/performanceStore'
import { FrameScheduler } from '../visualizers/frameScheduler'
import { getRendererWindowCapabilities } from '../windowCapabilities'
-export default function Strip(): JSX.Element {
+interface StripProps {
+ onMeasurementActiveChange?: (active: boolean) => void
+}
+
+export default function Strip({ onMeasurementActiveChange }: StripProps): JSX.Element {
const scopeOrder = useSettingsStore((s) => s.scopeOrder)
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
const scopePopouts = useSettingsStore((s) => s.scopePopouts)
@@ -268,6 +272,7 @@ export default function Strip(): JSX.Element {
))}
diff --git a/src/renderer/popouts/ScopePopoutWindow.tsx b/src/renderer/popouts/ScopePopoutWindow.tsx
index f63fb72..de7f85c 100644
--- a/src/renderer/popouts/ScopePopoutWindow.tsx
+++ b/src/renderer/popouts/ScopePopoutWindow.tsx
@@ -62,6 +62,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
const [snapshot, setSnapshot] = useState | null>(null)
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false)
const [cursorInsideWindow, setCursorInsideWindow] = useState(false)
+ const [measurementActive, setMeasurementActive] = useState(false)
const prevMiniSettingsOpenRef = useRef(false)
const frameTarget = usePerformanceStore((s) => s.frameTarget)
const miniSettingsOpen = useUiStore((s) => s.settingsOpen)
@@ -230,6 +231,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
'scope-popout__chrome',
miniSettingsOpen ? 'is-expanded' : '',
cursorInsideWindow ? 'is-cursor-inside' : '',
+ measurementActive ? 'is-measuring' : '',
].join(' ').trim()}
>
@@ -294,6 +296,7 @@ export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps)
settings={effectiveSettings}
frameScheduler={frameScheduler}
dataSource={dataSource}
+ onMeasurementActiveChange={setMeasurementActive}
/>
diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css
index 5613de8..a5870d6 100644
--- a/src/renderer/styles/globals.css
+++ b/src/renderer/styles/globals.css
@@ -2284,7 +2284,7 @@ button.toolbar__version:hover {
.scope-popout__viewport:hover .scope-popout__chrome,
.scope-popout__chrome:hover,
-.scope-popout__chrome:focus-within,
+.scope-popout__chrome:has(:focus-visible),
.scope-popout__chrome.is-cursor-inside,
.scope-popout__chrome.is-expanded {
max-height: 58px;
@@ -2293,6 +2293,13 @@ button.toolbar__version:hover {
transform: translateY(0);
}
+.scope-popout__viewport .scope-popout__chrome.is-measuring {
+ max-height: 0;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-8px);
+}
+
.scope-popout__header {
display: flex;
align-items: center;
diff --git a/test/renderer-helpers.test.ts b/test/renderer-helpers.test.ts
index a70e952..f4520e8 100644
--- a/test/renderer-helpers.test.ts
+++ b/test/renderer-helpers.test.ts
@@ -4212,6 +4212,38 @@ test('main and detached windows keep frameless Prism chrome while enabling snap-
assert.doesNotMatch(nowPlayingSource, /WindowResizeOverlay/)
})
+test('detached scope interactions accept the first macOS click and hide chrome during measurement', async () => {
+ const mainSource = await readFile(join(process.cwd(), 'src', 'main', 'index.ts'), 'utf8')
+ const scopeModuleSource = await readFile(join(process.cwd(), 'src', 'renderer', 'components', 'ScopeModule.tsx'), 'utf8')
+ const popoutSource = await readFile(join(process.cwd(), 'src', 'renderer', 'popouts', 'ScopePopoutWindow.tsx'), 'utf8')
+ const stylesSource = await readFile(join(process.cwd(), 'src', 'renderer', 'styles', 'globals.css'), 'utf8')
+ const mainWindowFactorySource = mainSource.slice(
+ mainSource.indexOf('function createMainWindow'),
+ mainSource.indexOf('function createScopePopoutWindow'),
+ )
+
+ assert.match(mainSource, /function createScopePopoutWindow\([\s\S]*?process\.platform === 'darwin' \? \{ acceptFirstMouse: true \} : \{\}/)
+ assert.doesNotMatch(mainWindowFactorySource, /acceptFirstMouse/)
+ assert.match(scopeModuleSource, /onMeasurementActiveChange\?: \(active: boolean\) => void/)
+ assert.match(scopeModuleSource, /onActiveChange: onMeasurementActiveChange/)
+ assert.match(popoutSource, /measurementActive \? 'is-measuring' : ''/)
+ assert.match(popoutSource, /onMeasurementActiveChange=\{setMeasurementActive\}/)
+ assert.match(stylesSource, /\.scope-popout__chrome:has\(:focus-visible\)/)
+ assert.doesNotMatch(stylesSource, /\.scope-popout__chrome:focus-within/)
+ assert.match(stylesSource, /\.scope-popout__viewport \.scope-popout__chrome\.is-measuring \{[\s\S]*?max-height: 0;[\s\S]*?opacity: 0;[\s\S]*?pointer-events: none;[\s\S]*?transform: translateY\(-8px\);[\s\S]*?\}/)
+})
+
+test('main window hides its toolbar while a docked scope measurement is active', async () => {
+ const appSource = await readFile(join(process.cwd(), 'src', 'renderer', 'App.tsx'), 'utf8')
+ const stripSource = await readFile(join(process.cwd(), 'src', 'renderer', 'components', 'Strip.tsx'), 'utf8')
+
+ assert.match(stripSource, /onMeasurementActiveChange\?: \(active: boolean\) => void/)
+ assert.match(stripSource, /onMeasurementActiveChange=\{onMeasurementActiveChange\}/)
+ assert.match(appSource, /const \[measurementActive, setMeasurementActive\] = useState\(false\)/)
+ assert.match(appSource, /toolbarVisible && !measurementActive \? 'is-visible' : ''/)
+ assert.match(appSource, //)
+})
+
test('programmatic top/bottom reposition flushes fresh bounds through persistence channels', async () => {
const mainSource = await readFile(join(process.cwd(), 'src', 'main', 'index.ts'), 'utf8')