diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index bf8b17438..7f6d355de 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -94,7 +94,7 @@ They contain no application state. renders `LayerRows` synchronously; keyed chips and list items keep focus through the refresh their own click triggers, and clicks re-read the live descriptor before acting), Display (in progress: `DisplayPanel` renders - bloom, sharpen and detection from the `gev-display` store, which VisualSettings + the HUD, Sonar, detection, bloom and sharpen from the `gev-display` store, which VisualSettings publishes to; the other groups follow one owner at a time), Context and Radio, the command dock, the phone shell, then Cockpit. Layers that write into the right rail diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 4c4a0f84d..23457616e 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -116,6 +116,7 @@ export class StyleManager extends ShellFacade { this._lifetime = new UiLifetime(); this._recording = new RecordingControls({ syncShareState: () => this._syncShareState(), + syncHudControls: () => this._visualSettings._syncHudControls(), }); Object.assign(this, readShellElements()); this._panelChrome = new PanelChrome({ @@ -209,20 +210,6 @@ export class StyleManager extends ShellFacade { elements: { _celestialBtn: this._celestialBtn, _cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn, - _hudBtn: this._hudBtn, - _cyberSonarBtn: this._cyberSonarBtn, - _cyberSonarRings: this._cyberSonarRings, - _cyberSonarRingsValue: this._cyberSonarRingsValue, - _cyberSonarRange: this._cyberSonarRange, - _cyberSonarRangeValue: this._cyberSonarRangeValue, - _cyberSonarIntensity: this._cyberSonarIntensity, - _cyberSonarIntensityValue: this._cyberSonarIntensityValue, - _cyberSonarOpacity: this._cyberSonarOpacity, - _cyberSonarOpacityValue: this._cyberSonarOpacityValue, - _cyberSonarSector: this._cyberSonarSector, - _cyberSonarSectorValue: this._cyberSonarSectorValue, - _hudLayoutRow: this._hudLayoutRow, - _hudLayoutSelect: this._hudLayoutSelect, _ppToggles: this._ppToggles, _scopeBtn: this._scopeBtn, _scopeFeatherSlider: this._scopeFeatherSlider, @@ -1693,9 +1680,6 @@ export class StyleManager extends ShellFacade { */ _initHUDToggle() { - if (this._hudLayoutSelect) { - this._hudLayoutSelect.value = 'tactical'; - } this._setHudVariant('tactical'); this.hud.setMode('on'); this._updateHudButtonState(); diff --git a/src/ui/cyberSonarControls.test.mjs b/src/ui/cyberSonarControls.test.mjs index 5ab764e80..4150fc367 100644 --- a/src/ui/cyberSonarControls.test.mjs +++ b/src/ui/cyberSonarControls.test.mjs @@ -48,6 +48,8 @@ test('real runner and facade use visual settings, preserve visibility and return const renders = []; const visual = Object.create(VisualSettings.prototype); visual.services = { governorRequestRender: (reason) => renders.push(reason) }; + // No Display panel here: the controls' render is not under test. + visual._publishDisplay = () => {}; const owner = Object.create(StyleManager.prototype); owner._visualSettings = visual; Object.defineProperty(owner, 'hud', { value: f.owner.hud }); diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index 67581f51c..c33089fd0 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -65,6 +65,7 @@ function styleOwner(t, initialVariant = 'cyber') { const owner = Object.create(VisualSettings.prototype); Object.assign(owner, { activeStyle: 'normal', + store: { publish() {} }, readHud: () => hud, readShareLinks: () => ({ onStyleChange() {} }), _visualEffects: { @@ -76,7 +77,6 @@ function styleOwner(t, initialVariant = 'cyber') { ), }, services: { governorRequestRender() {}, setDetectionStyle() {} }, - _hudLayoutSelect: { value: initialVariant }, _styleIndicator: {}, _setBloomEnabled: (value) => calls.push(['bloom', value]), _setSharpenEnabled: (value) => calls.push(['sharpen', value]), @@ -116,7 +116,6 @@ test('visual styles retain Cyber through real preset application without droppin assert.equal(owner.activeStyle, style); assert.equal(hud.getVariant(), 'cyber'); assert.equal(root.dataset.uiTheme, 'cyber'); - assert.equal(owner._hudLayoutSelect.value, 'cyber'); assert.deepEqual(calls.at(-1), ['share', 'cyber']); } assert.equal(owner.stages.thermal.uniforms.sensitivity, 0.85); diff --git a/src/ui/displayPanel.test.mjs b/src/ui/displayPanel.test.mjs index 5ed25f718..d0c70e614 100644 --- a/src/ui/displayPanel.test.mjs +++ b/src/ui/displayPanel.test.mjs @@ -205,3 +205,51 @@ test('detection renders its mode, rows, sliders and allocation', async (t) => { assert.equal(button.className, 'pp-toggle-btn'); assert.equal($('detection-slider-row').classList.contains('visible'), false); }); + +test('the HUD and Sonar controls render what VisualSettings shows', async (t) => { + const { store, $ } = fixture(t); + const { owner } = settings(store); + let visible = true; + let variant = 'minimal'; + owner.readHud = () => ({ + get visible() { + return visible; + }, + getVariant: () => variant, + }); + owner._scheduleAdaptivePanelLayout = () => {}; + assert.equal($('cyber-sonar-toggle').textContent, 'ON'); + assert.equal($('cyber-sonar-toggle').getAttribute('aria-pressed'), 'true'); + + owner._syncHudControls(); + await nextTick(); + assert.equal($('hud-toggle').classList.contains('active'), true); + assert.equal($('hud-layout-row').classList.contains('visible'), true); + assert.equal($('hud-layout-select').value, 'minimal'); + + visible = false; + owner._updateHudButtonState(); + await nextTick(); + assert.equal($('hud-toggle').classList.contains('active'), false); + assert.equal($('hud-layout-row').classList.contains('visible'), false); + + owner._sonarShown = { + enabled: false, + rings: 5, + range: 90, + intensity: 40, + opacity: 60, + sector: 30, + }; + owner._publishDisplay(); + await nextTick(); + const toggle = $('cyber-sonar-toggle'); + assert.equal(toggle.textContent, 'OFF'); + assert.equal(toggle.getAttribute('aria-pressed'), 'false'); + assert.equal(toggle.classList.contains('active'), false); + assert.equal($('cyber-sonar-rings').value, '5'); + assert.equal($('cyber-sonar-rings-value').textContent, '5'); + assert.equal($('cyber-sonar-range-value').textContent, '90%'); + assert.equal($('cyber-sonar-sector').value, '30'); + assert.equal($('cyber-sonar-sector-value').textContent, '30°'); +}); diff --git a/src/ui/rail/rangeValue.js b/src/ui/rail/rangeValue.js index b11744471..0ac3af74d 100644 --- a/src/ui/rail/rangeValue.js +++ b/src/ui/rail/rangeValue.js @@ -1,5 +1,6 @@ /** - * `v-range-value`: a range input's live value, written only when it differs. + * `v-range-value`: a range input's (or select's) live value, written only + * when it differs. * A `:value` binding would also rewrite the value attribute on every patch; * the directive runs after the element's min, max and step are patched, so * the value is never clamped to a stale range. diff --git a/src/ui/recordingControls.js b/src/ui/recordingControls.js index eb50c9a1b..f3e30fc06 100644 --- a/src/ui/recordingControls.js +++ b/src/ui/recordingControls.js @@ -1,7 +1,9 @@ /** Own recording chrome, safe frames and the pre-recording HUD snapshot. */ export class RecordingControls { - constructor({ syncShareState }) { + constructor({ syncShareState, syncHudControls }) { this._syncShareState = syncShareState; + // Shows the HUD's visibility and layout on the Display controls. + this._syncHudControls = syncHudControls; this.destroyed = false; this._recordingMode = false; this._recordingConfig = { @@ -12,8 +14,6 @@ export class RecordingControls { this._preRecordingHudState = null; this._safeFrameOverlay = document.getElementById('safe-frame-overlay'); this._safeFrameBox = document.getElementById('safe-frame-box'); - this._hudLayoutSelect = document.getElementById('hud-layout-select'); - this._hudBtn = document.getElementById('hud-toggle'); this.hud = null; } _initRecordingOverlay() { @@ -63,8 +63,6 @@ export class RecordingControls { } else if (hudMode === 'full' || hudMode === 'minimal') { this.hud.setMode('on'); this.hud.setVariant(hudMode === 'minimal' ? 'minimal' : 'tactical'); - if (this._hudLayoutSelect) - this._hudLayoutSelect.value = this.hud.getVariant(); } else { this.hud.setMode('auto'); } @@ -73,8 +71,6 @@ export class RecordingControls { this._preRecordingHudState = null; if (saved) { this.hud.setVariant(saved.variant); - if (this._hudLayoutSelect) - this._hudLayoutSelect.value = this.hud.getVariant(); } this.hud.setMode(saved ? saved.mode : 'auto'); if (this._safeFrameOverlay) { @@ -85,7 +81,7 @@ export class RecordingControls { ); } } - this._hudBtn.classList.toggle('active', this.hud.visible); + this._syncHudControls(); this._syncShareState(); } destroy() { diff --git a/src/ui/shellElements.js b/src/ui/shellElements.js index 683987c4f..c1d02e31a 100644 --- a/src/ui/shellElements.js +++ b/src/ui/shellElements.js @@ -11,23 +11,13 @@ export function readShellElements(document = globalThis.document) { _sharpenBtn: document.getElementById('sharpen-toggle'), _sharpenSlider: document.getElementById('sharpen-intensity-slider'), _hudBtn: document.getElementById('hud-toggle'), - _hudLayoutRow: document.getElementById('hud-layout-row'), _hudLayoutSelect: document.getElementById('hud-layout-select'), _cyberSonarBtn: document.getElementById('cyber-sonar-toggle'), _cyberSonarRings: document.getElementById('cyber-sonar-rings'), - _cyberSonarRingsValue: document.getElementById('cyber-sonar-rings-value'), _cyberSonarRange: document.getElementById('cyber-sonar-range'), - _cyberSonarRangeValue: document.getElementById('cyber-sonar-range-value'), _cyberSonarIntensity: document.getElementById('cyber-sonar-intensity'), - _cyberSonarIntensityValue: document.getElementById( - 'cyber-sonar-intensity-value', - ), _cyberSonarOpacity: document.getElementById('cyber-sonar-opacity'), - _cyberSonarOpacityValue: document.getElementById( - 'cyber-sonar-opacity-value', - ), _cyberSonarSector: document.getElementById('cyber-sonar-sector'), - _cyberSonarSectorValue: document.getElementById('cyber-sonar-sector-value'), _detectionDensitySlider: document.getElementById( 'detection-density-slider', ), diff --git a/src/ui/shellLifecycle.test.mjs b/src/ui/shellLifecycle.test.mjs index 37b02f1a5..ca94a4e60 100644 --- a/src/ui/shellLifecycle.test.mjs +++ b/src/ui/shellLifecycle.test.mjs @@ -200,7 +200,11 @@ test('recording restores the original HUD after repeated entry and revokes retai try { let mode = 'auto', variant = 'tactical'; - const owner = new RecordingControls({ syncShareState() {} }); + const hudSyncs = []; + const owner = new RecordingControls({ + syncShareState() {}, + syncHudControls: () => hudSyncs.push(variant), + }); owner.hud = { getMode: () => mode, getVariant: () => variant, @@ -218,6 +222,11 @@ test('recording restores the original HUD after repeated entry and revokes retai owner.setRecordingMode(true); assert.equal(mode, 'auto'); assert.equal(variant, 'tactical'); + assert.deepEqual( + hudSyncs, + ['minimal', 'minimal', 'tactical'], + 'each change shows the HUD layout it leaves', + ); assert.equal(document.body.classList.contains('recording-mode'), false); assert.equal( f.nodes.get('safe-frame-overlay').classList.contains('active'), diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index edceb479b..08dd6d9c3 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -5,11 +5,14 @@ import { t } from '../../../i18n/index.js'; import { vRangeValue } from '../../rail/rangeValue.js'; import { useDisplayStore } from '../../stores/display.js'; -// The Display panel. VisualSettings presents bloom, sharpen and detection -// and publishes them; until it does, the panel shows its initial markup. -// The other groups keep their owners' element bindings. +// The Display panel. VisualSettings presents the HUD, Sonar, detection, +// bloom and sharpen and publishes them; until it does, the panel shows its +// initial markup. The other groups keep their owners' element bindings. const { view } = storeToRefs(useDisplayStore()); const pct = (value) => `${value}%`; +const hud = computed(() => view.value?.hud ?? null); +const sonar = computed(() => view.value?.sonar ?? null); +const sonarOn = computed(() => sonar.value?.enabled ?? true); const detection = computed(() => view.value?.detection ?? null); const detectButton = computed(() => detection.value?.button ?? null); const allocation = computed(() => detection.value?.allocation ?? 'ELASTIC'); @@ -29,13 +32,13 @@ const allocationSegment = (value) => ({