feat(display): answer the Display controls through the store with UiSegmented

The Display panel, the dock's style buttons and the clean-view exit call
the display store's actions, and DisplayBindings connects their handlers
to it, replacing the bindDisplayControls listeners. The detection
allocation and 3D model coverage choices render through a new kit
UiSegmented, which takes v-model, options, label,
stacked and disabled, and adds a keyboard policy, item classes and
per-option attributes.

Shell element fields and facade accessors that only the old listeners
read are removed. The package boundaries now own UiSegmented and the
UiPanel kit files, and the format scope drops the deleted test.
This commit is contained in:
Sameh Khamis committed 2026-11-01 09:56:47 -08:00
1 parent fc53682129
commit 3c3f8e3e67
24 files changed
+624 -562

No files matched your search

-83
View File
@@ -1,86 +1,3 @@
import { createSegmentedControl } from './segmentedControl.js';
/**
* 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'],
['cleanViewButton', 'toggleCleanView'],
['cleanViewExitButton', 'exitCleanView'],
['celestialButton', 'toggleCelestial'],
['hudButton', 'toggleHud'],
['sonarButton', 'toggleSonar'],
['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.scopeToggle, 'change', 'setScopeEnabled', (el) => el.checked);
listen(elements.densitySlider, 'input', 'setDensity', (el) => el.value);
listen(elements.hudLayout, 'change', 'setHudLayout', (el) => el.value);
for (const [name, action] of [
['sonarRingsSlider', 'setSonarRings'],
['sonarRangeSlider', 'setSonarRange'],
['sonarIntensitySlider', 'setSonarIntensity'],
['sonarOpacitySlider', 'setSonarOpacity'],
['sonarSectorSlider', 'setSonarSector'],
])
listen(elements[name], 'input', action, integer);
for (const el of elements.styleButtons || [])
listen(el, 'click', 'setStyle', (el) => el.dataset.style);
for (const [buttons, valueOf, action] of [
[
elements.allocationButtons || [],
(el) => el.dataset.allocation,
'setAllocation',
],
[
elements.modelModeButtons || [],
(el) => (el.dataset.mode === 'all' ? 'all' : 'proximity'),
'setModelsMode',
],
]) {
const control = createSegmentedControl({
buttons,
valueOf,
onChange: (value) => actions[action](value),
});
removers.push(() => control.destroy());
}
listen(elements.fadeSlider, 'input', 'setFade', integer);
listen(elements.outsideOpacitySlider, 'input', 'setOutsideOpacity', integer);
// Hosts written before the two sliders were told apart pass `fadeSliders`
// and read both values in `setFade` themselves.
for (const el of elements.fadeSliders || []) {
if (el === elements.fadeSlider || el === elements.outsideOpacitySlider)
continue;
listen(el, 'input', 'setFade');
}
return {
destroy() {
for (const remove of removers.splice(0)) remove();
},
};
}
/**
* Keep a native Scope checkbox and feather availability in step with the
* mask. The Display view renders both from VisualSettings now; this stays