refactor(ui): own Display control subscriptions and cleanup

This commit is contained in:
Sameh Khamis committed 2026-09-12 21:25:04 -07:00
1 parent 795ed68192
commit ffc65dd844
11 files changed
+307 -166

No files matched your search

+3 -1
View File
@@ -246,7 +246,9 @@ test('detection-on-by-default is a default, not an operator override', () => {
// preset consults the flag, and the detection button sets it.
assert.match(uiSource, /if \(preset\.detection && !this\._detectionUserOverridden\) \{/,
'a style preset still yields to an operator who changed detection by hand');
const detectionButton = uiBlock("this._detectionBtn.addEventListener('click'", 'cycleDetectionMode()');
const displayActions = uiSource.slice(uiSource.indexOf('this._displayControls ='));
const detectionButton = displayActions.slice(displayActions.indexOf('cycleDetection:'), displayActions.indexOf('toggleModels:'));
assert.match(detectionButton, /cycleDetectionMode\(\)/, 'the button still invokes the detection action');
assert.match(detectionButton, /this\._detectionUserOverridden = true;/,
'and the detection control still claims the override when the operator uses it');
+15 -27
View File
@@ -419,16 +419,16 @@ test('every explicit visual UI gesture claims restore authority before it mutate
const gestureRoutes = [
['toggleHud: () => {', 'toggleOrbit: () =>', 'this.hud.toggle()', 'HUD hotkey'],
['cycleDetection: () => {', 'toggleCctv: () =>', 'cycleDetectionMode()', 'detection hotkey'],
['// Bloom toggle', '// Bloom intensity slider', 'this._setBloomEnabled(', 'bloom button'],
['// Bloom intensity slider', '// Sharpen toggle', 'this._setBloomIntensity(', 'bloom slider'],
['// Sharpen toggle', '// Scope mask', 'this._setSharpenEnabled(', 'sharpen button'],
["this._scopeBtn?.addEventListener('click'", "this._scopeFeatherSlider?.addEventListener('input'", 'setScopeMaskEnabled(', 'scope button'],
["this._scopeFeatherSlider?.addEventListener('input'", 'if (this._sharpenSlider)', 'setScopeMaskFeather(', 'scope feather slider'],
["this._sharpenSlider.addEventListener('input'", 'if (this._hudLayoutSelect)', 'this._applySharpenIntensity(', 'sharpen slider'],
["this._hudLayoutSelect.addEventListener('change'", 'if (this._cleanViewBtn)', 'this._setHudVariant(', 'HUD layout select'],
["this._detectionDensitySlider.addEventListener('input'", 'for (const button of this._detectionAllocationBtns)', 'this._applyDetectionDensityFromUi()', 'detection density slider'],
["button.addEventListener('click'", 'for (const slider of [this._detectionFadeSlider', 'this._setDetectionAllocation(', 'detection allocation button'],
["slider?.addEventListener('input'", 'if (this._celestialBtn)', 'this._applyDetectionFadeFromUi()', 'detection fade controls'],
['toggleBloom:', 'setBloomIntensity:', 'this._setBloomEnabled(', 'toggleBloom control'],
['setBloomIntensity:', 'toggleSharpen:', 'this._setBloomIntensity(', 'setBloomIntensity control'],
['toggleSharpen:', 'toggleScope:', 'this._setSharpenEnabled(', 'toggleSharpen control'],
['toggleScope:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'toggleScope control'],
['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'],
['setSharpenIntensity:', 'setHudLayout:', 'this._applySharpenIntensity(', 'setSharpenIntensity control'],
['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'],
['setDensity:', 'setAllocation:', 'this._applyDetectionDensityFromUi()', 'setDensity control'],
['setAllocation:', 'setFade:', 'this._setDetectionAllocation(', 'setAllocation control'],
['setFade:', 'toggleCelestial:', 'this._applyDetectionFadeFromUi()', 'setFade control'],
];
for (const [start, end, mutation, label] of gestureRoutes) {
const startIndex = initUi.indexOf(start);
@@ -437,23 +437,10 @@ test('every explicit visual UI gesture claims restore authority before it mutate
assertClaimsBefore(initUi.slice(startIndex, endIndex), mutation, label);
}
const hudToggle = sourceBlock(' _initHUDToggle() {', ' _initCockpitDisplayPortal() {');
assertClaimsBefore(
hudToggle.slice(
hudToggle.indexOf("this._hudBtn.addEventListener('click'"),
hudToggle.indexOf('if (this._hudLayoutSelect)'),
),
'this.hud.toggle()',
'HUD button',
);
assertClaimsBefore(
hudToggle.slice(
hudToggle.indexOf("this._detectionBtn.addEventListener('click'"),
hudToggle.indexOf('this._cockpitDisplayToggleBtn'),
),
'cycleDetectionMode()',
'detection button',
);
const displayActions = initUi.slice(initUi.indexOf('this._displayControls ='));
assertClaimsBefore(displayActions.slice(displayActions.indexOf('toggleHud:'), displayActions.indexOf('cycleDetection:')), 'this.hud.toggle()', 'HUD button');
assertClaimsBefore(displayActions.slice(displayActions.indexOf('cycleDetection:'), displayActions.indexOf('toggleModels:')), 'cycleDetectionMode()', 'detection button');
});
// Contacts OWNS detection while it is active (forced Dense @ 75%). That makes
@@ -703,5 +690,6 @@ test('visual input listeners are revoked before asynchronous UI teardown', () =>
assert.ok(firstAwait > 0);
const synchronous = disposal.slice(0, firstAwait);
assert.match(synchronous, /this\._applicationShortcuts\?\.destroy\(\)/);
assert.match(synchronous, /this\._displayControls\?\.destroy\(\)/);
assert.match(synchronous, /this\._styleParameters\?\.destroy\(\)/);
});
+110 -137
View File
@@ -1,3 +1,4 @@
import { bindDisplayControls } from './ui/displayControls.js';
import { bindApplicationShortcuts, createStyleParameters } from './ui/visualInput.js';
import { layoutLeftPanelRail, layoutRightPanelRail } from './ui/panelRails.js';
import { bindPanelDisclosure, collapsePanelOnEscape, createHoverDisclosure } from './ui/panelDisclosure.js';
@@ -3395,11 +3396,6 @@ export class StyleManager {
* @returns {void}
*/
_initUI() {
// Style buttons
document.querySelectorAll('.style-btn').forEach(btn => {
btn.addEventListener('click', () => this.setStyle(btn.dataset.style));
});
this._applicationShortcuts?.destroy();
this._applicationShortcuts = bindApplicationShortcuts({
documentRef: document,
@@ -3432,108 +3428,109 @@ export class StyleManager {
},
});
// Bloom toggle
this._bloomBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomEnabled(!this.bloomEnabled);
this._displayControls?.destroy();
this._displayControls = bindDisplayControls({
elements: {
styleButtons: document.querySelectorAll('.style-btn'),
bloomButton: this._bloomBtn, bloomSlider: this._bloomSlider,
sharpenButton: this._sharpenBtn, sharpenSlider: this._sharpenSlider,
scopeButton: this._scopeBtn, scopeFeatherSlider: this._scopeFeatherSlider,
hudLayout: this._hudLayoutSelect, hudButton: this._hudBtn,
cleanViewButton: this._cleanViewBtn, cleanViewExitButton: this._cleanViewExitBtn,
densitySlider: this._detectionDensitySlider, detectionButton: this._detectionBtn,
allocationButtons: this._detectionAllocationBtns,
fadeSliders: [this._detectionFadeSlider, this._detectionOpacitySlider],
celestialButton: this._celestialBtn,
modelsButton: this._models3dBtn,
modelModeButtons: this._models3dBtn ? this._models3dModeBtns : [],
},
actions: {
setStyle: (style) => this.setStyle(style),
toggleBloom: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomEnabled(!this.bloomEnabled);
},
setBloomIntensity: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomIntensity(value);
},
toggleSharpen: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setSharpenEnabled(!this.sharpenEnabled);
},
toggleScope: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const next = !isScopeMaskEnabled();
setScopeMaskEnabled(next);
this._scopeBtn.classList.toggle('active', next);
this._scopeBtn.setAttribute('aria-pressed', String(next));
this._syncShareState();
},
setScopeFeather: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const pct = Math.max(0, Math.min(100, value || 0));
if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`;
setScopeMaskFeather(pct / 100);
this._syncShareState();
},
setSharpenIntensity: (pct) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
if (this._sharpenSliderValue) this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
this._syncShareState();
},
setHudLayout: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setHudVariant(value);
},
toggleCleanView: () => this.toggleCleanView(),
exitCleanView: () => this.toggleCleanView(false),
setDensity: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
const pct = canonicalizeDensity(value);
this._detectionDensitySlider.value = String(pct);
if (this._detectionDensityValue) this._detectionDensityValue.textContent = `${pct}%`;
this._applyDetectionDensityFromUi();
this._syncShareState();
},
setAllocation: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
this._setDetectionAllocation(value);
},
setFade: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._applyDetectionFadeFromUi();
this._syncShareState();
},
toggleCelestial: () => {
const ringIsVisible = !!this.celestialRing?.visible;
if (!this.celestialRingEnabled || !ringIsVisible) {
this.setCelestialRingEnabled(true, { focus: true });
} else {
this.setCelestialRingEnabled(false);
}
},
toggleHud: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this.hud.toggle();
this._updateHudButtonState();
this._syncShareState();
},
cycleDetection: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
cycleDetectionMode();
this._syncShareState();
},
toggleModels: () => {
this._setModels3dEnabled(!this._models3dEnabled);
this._syncModels3dModeRow();
},
setModelsMode: (mode) => this._setModels3dMode(mode),
},
});
// Bloom intensity slider
this._bloomSlider.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomIntensity(parseInt(this._bloomSlider.value, 10));
});
// Sharpen toggle
this._sharpenBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setSharpenEnabled(!this.sharpenEnabled);
});
// Scope mask — the explicit circular viewport treatment (owner ask:
// standalone toggle + featherable edge; see src/scopeMask.js).
this._scopeBtn?.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const next = !isScopeMaskEnabled();
setScopeMaskEnabled(next);
this._scopeBtn.classList.toggle('active', next);
this._scopeBtn.setAttribute('aria-pressed', String(next));
this._syncShareState();
});
this._scopeFeatherSlider?.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const pct = Math.max(0, Math.min(100, parseInt(this._scopeFeatherSlider.value, 10) || 0));
if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`;
setScopeMaskFeather(pct / 100);
this._syncShareState();
});
if (this._sharpenSlider) {
this._sharpenSlider.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const pct = parseInt(this._sharpenSlider.value, 10);
if (this._sharpenSliderValue) {
this._sharpenSliderValue.textContent = `${pct}%`;
}
this._applySharpenIntensity(pct / 100);
this._syncShareState();
});
}
if (this._hudLayoutSelect) {
this._hudLayoutSelect.addEventListener('change', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setHudVariant(this._hudLayoutSelect.value);
});
}
if (this._cleanViewBtn) {
this._cleanViewBtn.addEventListener('click', () => this.toggleCleanView());
}
if (this._cleanViewExitBtn) {
this._cleanViewExitBtn.addEventListener('click', () => this.toggleCleanView(false));
}
if (this._detectionDensitySlider) {
this._detectionDensitySlider.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
const pct = canonicalizeDensity(this._detectionDensitySlider.value);
this._detectionDensitySlider.value = String(pct);
if (this._detectionDensityValue) {
this._detectionDensityValue.textContent = `${pct}%`;
}
this._applyDetectionDensityFromUi();
this._syncShareState();
});
}
for (const button of this._detectionAllocationBtns) {
button.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
this._setDetectionAllocation(button.dataset.allocation);
});
}
for (const slider of [this._detectionFadeSlider, this._detectionOpacitySlider]) {
slider?.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._applyDetectionFadeFromUi();
this._syncShareState();
});
}
if (this._celestialBtn) {
this._celestialBtn.addEventListener('click', () => {
const ringIsVisible = !!this.celestialRing?.visible;
if (!this.celestialRingEnabled || !ringIsVisible) {
this.setCelestialRingEnabled(true, { focus: true });
} else {
this.setCelestialRingEnabled(false);
}
});
}
}
/**
@@ -9395,26 +9392,15 @@ export class StyleManager {
this._layoutRightPanels();
}
_syncModels3dModeRow() {
if (this._models3dModeRow) this._models3dModeRow.classList.toggle('visible', this._models3dEnabled);
this._layoutRightPanels();
}
_initModels3dToggle() {
if (!this._models3dBtn) return;
// The Proximity/All mode row is revealed only while 3D is on (mirrors the DETECT slider row).
const syncModeRow = () => {
if (this._models3dModeRow) this._models3dModeRow.classList.toggle('visible', this._models3dEnabled);
this._layoutRightPanels();
};
this._models3dBtn.addEventListener('click', () => {
this._setModels3dEnabled(!this._models3dEnabled);
syncModeRow();
});
for (const btn of this._models3dModeBtns) {
if (!btn) continue;
btn.addEventListener('click', () => {
const mode = btn.dataset.mode === 'all' ? 'all' : 'proximity';
this._setModels3dMode(mode);
});
}
this._syncModels3dButtonState();
syncModeRow();
this._syncModels3dModeRow();
}
_setModels3dEnabled(enabled) {
@@ -9447,13 +9433,6 @@ export class StyleManager {
}
_initHUDToggle() {
this._hudBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this.hud.toggle();
this._updateHudButtonState();
this._syncShareState();
});
if (this._hudLayoutSelect) {
this._hudLayoutSelect.value = 'tactical';
}
@@ -9461,13 +9440,6 @@ export class StyleManager {
this.hud.setMode('on');
this._updateHudButtonState();
// Detection toggle button
this._detectionBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
cycleDetectionMode();
this._syncShareState();
});
this._cockpitDisplayToggleBtn?.addEventListener('click', () => {
const open = this._cockpitDisplayToggleBtn.getAttribute('aria-expanded') === 'true';
this._setCockpitDisclosure?.('display', !open);
@@ -9690,6 +9662,7 @@ export class StyleManager {
if (this._globalLoadingStatus) this._globalLoadingStatus.hidden = true;
this._disposed = true;
this._applicationShortcuts?.destroy();
this._displayControls?.destroy();
this._styleParameters?.destroy();
for (const control of this._panelDisclosureControls || []) control.destroy();
this._panelDisclosureControls = [];
+41
View File
@@ -0,0 +1,41 @@
/**
* Bind Display controls to application actions. Inputs remain native DOM controls;
* the caller owns settings and effect state. Destroy before replacing the view.
* @param {{ elements: object, actions: object }} options
* @returns {{ destroy(): void }}
*/
export function bindDisplayControls({ elements, actions }) {
const removers = [];
const listen = (element, type, action, read) => {
if (!element) return;
const handler = () => actions[action](...(read ? [read(element)] : []));
element.addEventListener(type, handler);
removers.push(() => element.removeEventListener(type, handler));
};
const integer = (element) => parseInt(element.value, 10);
for (const [name, action] of [
['bloomButton', 'toggleBloom'], ['sharpenButton', 'toggleSharpen'],
['scopeButton', 'toggleScope'], ['cleanViewButton', 'toggleCleanView'],
['cleanViewExitButton', 'exitCleanView'], ['celestialButton', 'toggleCelestial'],
['hudButton', 'toggleHud'], ['detectionButton', 'cycleDetection'],
['modelsButton', 'toggleModels'],
]) listen(elements[name], 'click', action);
for (const [name, action] of [
['bloomSlider', 'setBloomIntensity'], ['sharpenSlider', 'setSharpenIntensity'],
['scopeFeatherSlider', 'setScopeFeather'],
]) listen(elements[name], 'input', action, integer);
listen(elements.densitySlider, 'input', 'setDensity', (el) => el.value);
listen(elements.hudLayout, 'change', 'setHudLayout', (el) => el.value);
for (const el of elements.styleButtons || [])
listen(el, 'click', 'setStyle', (el) => el.dataset.style);
for (const el of elements.allocationButtons || [])
listen(el, 'click', 'setAllocation', (el) => el.dataset.allocation);
for (const el of elements.modelModeButtons || [])
listen(el, 'click', 'setModelsMode', (el) => el.dataset.mode === 'all' ? 'all' : 'proximity');
for (const el of elements.fadeSliders || []) listen(el, 'input', 'setFade');
return {
destroy() {
for (const remove of removers.splice(0)) remove();
},
};
}
+77
View File
@@ -0,0 +1,77 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { bindDisplayControls } from './displayControls.js';
function element(value = '') {
const el = new EventTarget();
el.value = value;
el.dataset = {};
return el;
}
test('controls read current values without preventing native input behavior', () => {
const bloomSlider = element('24');
const hudLayout = element('tactical');
const calls = [];
const control = bindDisplayControls({ elements: { bloomSlider, hudLayout }, actions: {
setBloomIntensity: (value) => calls.push(value),
setHudLayout: (value) => calls.push(value),
} });
const event = new Event('input', { cancelable: true });
bloomSlider.dispatchEvent(event);
bloomSlider.value = '37';
bloomSlider.dispatchEvent(new Event('input'));
hudLayout.value = 'minimal';
hudLayout.dispatchEvent(new Event('change'));
assert.deepEqual(calls, [24, 37, 'minimal']);
assert.equal(event.defaultPrevented, false);
control.destroy();
});
test('destroyed and replaced controls cannot issue stale actions', () => {
const bloomButton = element();
let oldCalls = 0;
let newCalls = 0;
const first = bindDisplayControls({ elements: { bloomButton }, actions: { toggleBloom: () => oldCalls++ } });
bloomButton.dispatchEvent(new Event('click'));
first.destroy();
const second = bindDisplayControls({ elements: { bloomButton }, actions: { toggleBloom: () => newCalls++ } });
first.destroy();
bloomButton.dispatchEvent(new Event('click'));
assert.equal(oldCalls, 1);
assert.equal(newCalls, 1);
second.destroy();
bloomButton.dispatchEvent(new Event('click'));
assert.equal(newCalls, 1);
});
test('style, allocation and model choices retain their current data attributes', () => {
const style = element();
const allocation = element();
const mode = element();
style.dataset.style = 'thermal';
allocation.dataset.allocation = 'balanced';
mode.dataset.mode = 'all';
const calls = [];
const control = bindDisplayControls({ elements: { styleButtons: [style], allocationButtons: [allocation], modelModeButtons: [mode] }, actions: {
setStyle: (value) => calls.push(value), setAllocation: (value) => calls.push(value), setModelsMode: (value) => calls.push(value),
} });
for (const el of [style, allocation, mode]) el.dispatchEvent(new Event('click'));
mode.dataset.mode = 'unknown';
mode.dispatchEvent(new Event('click'));
assert.deepEqual(calls, ['thermal', 'balanced', 'all', 'proximity']);
control.destroy();
});
test('optional controls are absent safely and subscriptions stay instance-owned', () => {
const calls = [];
const a = element();
const b = element();
const one = bindDisplayControls({ elements: { fadeSliders: [null, a] }, actions: { setFade: () => calls.push('a') } });
const two = bindDisplayControls({ elements: { fadeSliders: [b] }, actions: { setFade: () => calls.push('b') } });
one.destroy();
a.dispatchEvent(new Event('input'));
b.dispatchEvent(new Event('input'));
assert.deepEqual(calls, ['b']);
two.destroy();
});