diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 7f6d355de..fdc538b12 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -94,9 +94,9 @@ 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 - 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 HUD, Sonar, Scope, Celestial, 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 directly gain a slot instead. diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index 71c656476..105b8a573 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -520,8 +520,8 @@ test('every explicit visual UI gesture claims restore authority before it mutate ['toggleBloom:', 'setBloomIntensity:', 'this._setBloomEnabled(', 'toggleBloom control'], ['setBloomIntensity:', 'toggleSharpen:', 'this._setBloomIntensity(', 'setBloomIntensity control'], ['toggleSharpen:', 'setScopeEnabled:', 'this._setSharpenEnabled(', 'toggleSharpen control'], - ['setScopeEnabled:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'setScopeEnabled control'], - ['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'], + ['setScopeEnabled:', 'setScopeFeather:', 'this._setScope(', 'setScopeEnabled control'], + ['setScopeFeather:', 'setSharpenIntensity:', 'this._setScope(', 'setScopeFeather control'], ['setSharpenIntensity:', 'setHudLayout:', 'this._setSharpenIntensity(', 'setSharpenIntensity control'], ['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'], ['setDensity:', 'setAllocation:', 'this._setDetectionDensity(', 'setDensity control'], diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 23457616e..284d7dc01 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -208,12 +208,8 @@ export class StyleManager extends ShellFacade { setScopeMaskFeather: services.setScopeMaskFeather, }, elements: { - _celestialBtn: this._celestialBtn, _cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn, _ppToggles: this._ppToggles, - _scopeBtn: this._scopeBtn, - _scopeFeatherSlider: this._scopeFeatherSlider, - _scopeFeatherValue: this._scopeFeatherValue, _sliderContainer: this._sliderContainer, _sliderPanel: this._sliderPanel, _styleIndicator: this._styleIndicator, @@ -491,9 +487,7 @@ export class StyleManager extends ShellFacade { viewer, services: { cycleDetectionMode: services.cycleDetectionMode, - setScopeMaskEnabled: services.setScopeMaskEnabled, isScopeMaskEnabled: services.isScopeMaskEnabled, - setScopeMaskFeather: services.setScopeMaskFeather, }, elements: { _locationSearch: this._locationSearch, @@ -520,7 +514,6 @@ export class StyleManager extends ShellFacade { _detectionAllocationBtns: this._detectionAllocationBtns, _celestialBtn: this._celestialBtn, _models3dBtn: this._models3dBtn, - _scopeFeatherValue: this._scopeFeatherValue, }, operations: { setStyle: (...args) => this.setStyle(...args), @@ -540,6 +533,7 @@ export class StyleManager extends ShellFacade { _setDetectionAllocation: (...args) => this._setDetectionAllocation(...args), _setDetectionFade: (...args) => this._setDetectionFade(...args), + _setScope: (...args) => this._visualSettings._setScope(...args), setCelestialRingEnabled: (...args) => this.setCelestialRingEnabled(...args), _setModels3dEnabled: (...args) => this._setModels3dEnabled(...args), diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index c33089fd0..ef0d1c8fa 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -279,28 +279,23 @@ test('scope choice and expanded feather survive every HUD layout and both restor const previous = { enabled: isScopeMaskEnabled(), feather: getScopeMaskFeather() }; t.after(() => { setScopeMaskEnabled(previous.enabled); setScopeMaskFeather(previous.feather); }); Object.assign(owner.services, { setScopeMaskEnabled, setScopeMaskFeather }); - owner._scopeBtn = { checked: true }; - owner._scopeFeatherSlider = { value: '11', disabled: false }; - owner._scopeFeatherValue = {}; + let shown = null; + owner.store = { publish: (view) => (shown = view.scope) }; await owner.restoreShareState({ scopeEnabled: false, scopeFeatherPct: 200 }); - assert.equal(owner._scopeBtn.checked, false); - assert.equal(owner._scopeFeatherSlider.disabled, true); - assert.equal(owner._scopeFeatherSlider.value, '200'); + assert.deepEqual(shown, { enabled: false, featherPct: 200 }); for (const layout of ['minimal', 'cyber', 'tactical']) { owner._setHudVariant(layout, { applyVisualDefaults: true }); assert.equal(isScopeMaskEnabled(), false, layout); assert.equal(getScopeMaskFeather(), 2, layout); - assert.equal(owner._scopeBtn.checked, false, layout); + assert.equal(shown.enabled, false, layout); } await owner.applyVisualState({ scope: { enabled: true, featherPct: 175 } }); assert.equal(isScopeMaskEnabled(), true); assert.equal(getScopeMaskFeather(), 1.75); - assert.equal(owner._scopeBtn.checked, true); - assert.equal(owner._scopeFeatherSlider.disabled, false); - assert.equal(owner._scopeFeatherValue.textContent, '175%'); + assert.deepEqual(shown, { enabled: true, featherPct: 175 }); await owner.applyVisualState({ scope: { featherPct: 500 } }); assert.equal(getScopeMaskFeather(), 2); - assert.equal(owner._scopeFeatherSlider.value, '200'); + assert.deepEqual(shown, { enabled: true, featherPct: 200 }); }); test('a scene shot saved with the retired Snow style plays on Normal', async (t) => { diff --git a/src/ui/displayBindings.js b/src/ui/displayBindings.js index 181f98eae..e2f213c02 100644 --- a/src/ui/displayBindings.js +++ b/src/ui/displayBindings.js @@ -1,7 +1,6 @@ -import { clampScopeFeatherPct } from '../scopeMaskPolicy.js'; import { createFrameRateMonitor } from './frameRateMonitor.js'; import { bindApplicationShortcuts } from './visualInput.js'; -import { bindDisplayControls, syncScopeControls } from './displayControls.js'; +import { bindDisplayControls } from './displayControls.js'; /** Own keyboard/display event subscriptions; settings remain with their state owners. */ export class DisplayBindings { @@ -45,12 +44,7 @@ export class DisplayBindings { return this.readState()._models3dModeBtns; } _initUI() { - const { - cycleDetectionMode, - setScopeMaskEnabled, - isScopeMaskEnabled, - setScopeMaskFeather, - } = this.services; + const { cycleDetectionMode, isScopeMaskEnabled } = this.services; this._applicationShortcuts?.destroy(); this._frameRateMonitor?.destroy(); this._frameRateMonitor = createFrameRateMonitor({ @@ -89,11 +83,7 @@ export class DisplayBindings { }, }); - syncScopeControls( - this._scopeBtn, - this._scopeFeatherSlider, - isScopeMaskEnabled(), - ); + this._setScope({ enabled: isScopeMaskEnabled() }, { apply: false }); this._displayControls?.destroy(); this._displayControls = bindDisplayControls({ elements: { @@ -139,16 +129,12 @@ export class DisplayBindings { }, setScopeEnabled: (enabled) => { this.shareLinkManager?.claimRestoreLane?.('visual'); - setScopeMaskEnabled(enabled); - syncScopeControls(this._scopeBtn, this._scopeFeatherSlider, enabled); + this._setScope({ enabled }); this._syncShareState(); }, setScopeFeather: (value) => { this.shareLinkManager?.claimRestoreLane?.('visual'); - const pct = clampScopeFeatherPct(value); - if (this._scopeFeatherValue) - this._scopeFeatherValue.textContent = `${pct}%`; - setScopeMaskFeather(pct / 100); + this._setScope({ featherPct: value }); this._syncShareState(); }, setSharpenIntensity: (pct) => { diff --git a/src/ui/displayControls.js b/src/ui/displayControls.js index ffa009ed9..3969e46f1 100644 --- a/src/ui/displayControls.js +++ b/src/ui/displayControls.js @@ -73,9 +73,3 @@ export function bindDisplayControls({ elements, actions }) { }, }; } - -/** Keep the native Scope checkbox and feather availability in step with the mask. */ -export function syncScopeControls(toggle, featherSlider, enabled) { - if (toggle) toggle.checked = enabled; - if (featherSlider) featherSlider.disabled = !enabled; -} diff --git a/src/ui/displayPanel.test.mjs b/src/ui/displayPanel.test.mjs index d0c70e614..47a0ab7da 100644 --- a/src/ui/displayPanel.test.mjs +++ b/src/ui/displayPanel.test.mjs @@ -253,3 +253,47 @@ test('the HUD and Sonar controls render what VisualSettings shows', async (t) => assert.equal($('cyber-sonar-sector').value, '30'); assert.equal($('cyber-sonar-sector-value').textContent, '30°'); }); + +test('Scope and Celestial render what VisualSettings shows', async (t) => { + const { store, $ } = fixture(t); + const { owner } = settings(store); + const mask = { enabled: true, feather: 0.35 }; + owner.services = { + ...owner.services, + setScopeMaskEnabled: (on) => (mask.enabled = on), + setScopeMaskFeather: (value) => (mask.feather = value), + }; + assert.equal($('scope-toggle').checked, true); + assert.equal($('scope-feather-value').textContent, '100%'); + assert.equal($('celestial-toggle').hasAttribute('aria-disabled'), false); + + owner._setScope({ enabled: false }, { apply: false }); + await nextTick(); + assert.equal($('scope-toggle').checked, false); + assert.equal($('scope-feather-slider').disabled, true); + assert.equal(mask.enabled, true, 'only shown'); + + owner._setScope({ enabled: true, featherPct: 250 }); + await nextTick(); + assert.equal($('scope-toggle').checked, true); + assert.equal($('scope-feather-slider').disabled, false); + assert.equal($('scope-feather-slider').value, '200', 'clamped'); + assert.equal($('scope-feather-value').textContent, '200%'); + assert.deepEqual(mask, { enabled: true, feather: 2 }); + + owner._celestialShown = { active: false, supported: false }; + owner._publishDisplay(); + await nextTick(); + const celestial = $('celestial-toggle'); + assert.equal(celestial.disabled, true); + assert.equal(celestial.getAttribute('aria-disabled'), 'true'); + assert.equal(celestial.title, 'Celestial ring — available in Normal style'); + + owner._celestialShown = { active: true, supported: true }; + owner._publishDisplay(); + await nextTick(); + assert.equal(celestial.disabled, false); + assert.equal(celestial.getAttribute('aria-pressed'), 'true'); + assert.equal(celestial.classList.contains('active'), true); + assert.equal(celestial.title, 'Celestial ring — reveal the full globe'); +}); diff --git a/src/ui/shellElements.js b/src/ui/shellElements.js index c1d02e31a..92f68ce36 100644 --- a/src/ui/shellElements.js +++ b/src/ui/shellElements.js @@ -32,7 +32,6 @@ export function readShellElements(document = globalThis.document) { _celestialBtn: document.getElementById('celestial-toggle'), _scopeBtn: document.getElementById('scope-toggle'), _scopeFeatherSlider: document.getElementById('scope-feather-slider'), - _scopeFeatherValue: document.getElementById('scope-feather-value'), _mapStackChips: document.getElementById('map-stack-chips'), _mapStackStatus: document.getElementById('map-stack-status'), _cleanViewBtn: document.getElementById('clean-view-toggle'), diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index 08dd6d9c3..008c84d33 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -5,9 +5,9 @@ import { t } from '../../../i18n/index.js'; import { vRangeValue } from '../../rail/rangeValue.js'; import { useDisplayStore } from '../../stores/display.js'; -// 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. +// The Display panel. VisualSettings presents the HUD, Sonar, Scope, +// Celestial, 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); @@ -16,6 +16,14 @@ 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'); +const scopeOn = computed(() => view.value?.scope.enabled ?? true); +const scopeFeather = computed(() => view.value?.scope.featherPct ?? 100); +const celestial = computed(() => view.value?.celestial ?? null); +const celestialTitle = computed(() => + celestial.value && !celestial.value.supported + ? t('Celestial ring — available in Normal style') + : t('Celestial ring — reveal the full globe'), +); const allocationSegment = (value) => ({ class: { active: allocation.value === value }, 'aria-checked': String(allocation.value === value), @@ -133,12 +141,12 @@ const allocationSegment = (value) => ({