mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
refactor: compose UI shell with explicit application operations and component owners
This commit is contained in:
1 parent
8a226a2fe3
commit
f5ef9091f0
67 files changed
+15799
-15059
No files matched your search
@@ -0,0 +1,386 @@
|
||||
/** Own rail measurement, layout scheduling and dock tray observation. */
|
||||
import { layoutLeftPanelRail, layoutRightPanelRail } from './panelRails.js';
|
||||
const COCKPIT_LAYOUT_SETTLE_MS = 240;
|
||||
const LEFT_STACK_OBSTACLE_SELECTOR = [
|
||||
'#cockpit-hud .cockpit-topline',
|
||||
'#cockpit-hud .cockpit-topline > div',
|
||||
'#title-bar',
|
||||
'#style-indicator',
|
||||
'#top-center-actions',
|
||||
'#traffic-sync-chip',
|
||||
'#cctv-sync-chip',
|
||||
'#intel-hud .hud-top-left',
|
||||
'#intel-hud .hud-top-right',
|
||||
'#intel-hud .hud-bottom-left',
|
||||
'#intel-hud .hud-bottom-right',
|
||||
'#intel-hud .hud-top-bar',
|
||||
'#intel-hud .hud-bottom-bar',
|
||||
'#intel-hud .hud-left-edge',
|
||||
'#intel-hud .hud-right-edge',
|
||||
'#cockpit-context',
|
||||
'#cesium-credits .cesium-credit-logoContainer',
|
||||
'#cesium-credits .cesium-credit-textContainer',
|
||||
'#location-bar',
|
||||
'#control-panel',
|
||||
'#gev-voice-control',
|
||||
'#pp-toggles',
|
||||
'#param-slider-panel',
|
||||
].join(', ');
|
||||
const RIGHT_STACK_OBSTACLE_SELECTOR = [
|
||||
'#cockpit-hud .cockpit-topline',
|
||||
'#cockpit-hud .cockpit-topline > div',
|
||||
'#title-bar',
|
||||
'#style-indicator',
|
||||
'#top-center-actions',
|
||||
'#traffic-sync-chip',
|
||||
'#cctv-sync-chip',
|
||||
'#intel-hud .hud-top-left',
|
||||
'#intel-hud .hud-top-right',
|
||||
'#intel-hud .hud-bottom-left',
|
||||
'#intel-hud .hud-bottom-right',
|
||||
'#intel-hud .hud-top-bar',
|
||||
'#intel-hud .hud-bottom-bar',
|
||||
'#intel-hud .hud-left-edge',
|
||||
'#intel-hud .hud-right-edge',
|
||||
'#cockpit-context',
|
||||
'#cockpit-signal-stream',
|
||||
'#cesium-credits .cesium-credit-logoContainer',
|
||||
'#cesium-credits .cesium-credit-textContainer',
|
||||
'#command-dock',
|
||||
'#gev-voice-control',
|
||||
].join(', ');
|
||||
export class PanelLayoutController {
|
||||
constructor({ readHud, scheduleCockpitLayout, syncPanelCollapseButton, readDisplayScrollTop }) {
|
||||
this.readHud = readHud;
|
||||
this.scheduleCockpitLayout = scheduleCockpitLayout;
|
||||
this._syncPanelCollapseButton = syncPanelCollapseButton;
|
||||
this.readDisplayScrollTop = readDisplayScrollTop;
|
||||
this.destroyed = false;
|
||||
this._adaptivePanelSettleTimer = null;
|
||||
this._commandDockTrayObserver = null;
|
||||
this._leftStackCockpitModeHandler = null;
|
||||
this._leftStackCollapsedHeights = new Map();
|
||||
this._leftStackHudTransitionHandler = null;
|
||||
this._leftStackLayoutFrame = null;
|
||||
this._leftStackMutationObserver = null;
|
||||
this._leftStackPreferredPanelId = null;
|
||||
this._leftStackReconsiderAutoCollapse = false;
|
||||
this._leftStackResizeObserver = null;
|
||||
this._rightStackHudTransitionHandler = null;
|
||||
this._rightStackLayoutFrame = null;
|
||||
this._rightStackMutationObserver = null;
|
||||
this._rightStackPreferredPanelId = null;
|
||||
this._rightStackReconsiderAutoCollapse = false;
|
||||
this._rightStackResizeObserver = null;
|
||||
this._cockpitLayoutFrame = null;
|
||||
this._cockpitLayoutTimer = null;
|
||||
this._leftPanelStack = document.getElementById('left-panel-stack');
|
||||
this._rightPanelStack = document.getElementById('right-context-rail');
|
||||
this._ppToggles = document.getElementById('pp-toggles');
|
||||
this._cctvPanel = document.getElementById('cctv-panel');
|
||||
this._sliderPanel = document.getElementById('param-slider-panel');
|
||||
this._detectionBtn = document.getElementById('detection-toggle');
|
||||
}
|
||||
_scheduleAdaptivePanelLayout({ settle = false } = {}) {
|
||||
if (this.destroyed) return;
|
||||
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
|
||||
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
|
||||
this.scheduleCockpitLayout();
|
||||
if (!settle) return;
|
||||
clearTimeout(this._adaptivePanelSettleTimer);
|
||||
this._adaptivePanelSettleTimer = setTimeout(() => {
|
||||
if (this.destroyed) return;
|
||||
this._adaptivePanelSettleTimer = null;
|
||||
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
|
||||
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
|
||||
this.scheduleCockpitLayout();
|
||||
}, COCKPIT_LAYOUT_SETTLE_MS);
|
||||
}
|
||||
|
||||
_initCommandDockTrayMetrics() {
|
||||
if (this.destroyed) return;
|
||||
const dock = document.getElementById('command-dock');
|
||||
if (!dock) return;
|
||||
this._commandDockTrayObserver?.disconnect?.();
|
||||
if (typeof ResizeObserver === 'function') {
|
||||
this._commandDockTrayObserver = new ResizeObserver(() => this._updateCommandDockTrayStack());
|
||||
dock.querySelectorAll('.dock-popover-content').forEach((tray) => {
|
||||
this._commandDockTrayObserver.observe(tray);
|
||||
});
|
||||
}
|
||||
this._updateCommandDockTrayStack();
|
||||
}
|
||||
|
||||
_updateCommandDockTrayStack() {
|
||||
if (this.destroyed) return;
|
||||
const dock = document.getElementById('command-dock');
|
||||
if (!dock) return;
|
||||
const locationPanel = dock.querySelector('#location-bar.dock-pinned:not(.collapsed)');
|
||||
const presetsPanel = dock.querySelector('#control-panel.dock-pinned:not(.collapsed)');
|
||||
const locationHeight = locationPanel?.querySelector('.dock-popover-content')?.getBoundingClientRect().height || 0;
|
||||
const presetsHeight = presetsPanel?.querySelector('.dock-popover-content')?.getBoundingClientRect().height || 0;
|
||||
const pinnedCount = Number(locationHeight > 0) + Number(presetsHeight > 0);
|
||||
const locationHeightPx = Math.ceil(locationHeight);
|
||||
const presetsHeightPx = Math.ceil(presetsHeight);
|
||||
const topPinnedPanel = dock.querySelector('.dock-pinned-top.dock-pinned:not(.collapsed)');
|
||||
const lowerPinnedPanel = topPinnedPanel?.id === 'location-bar' ? presetsPanel : locationPanel;
|
||||
const lowerPinnedHeight = lowerPinnedPanel
|
||||
?.querySelector('.dock-popover-content')
|
||||
?.getBoundingClientRect().height || 0;
|
||||
const stackHeight = pinnedCount > 1
|
||||
? `calc(${locationHeightPx}px + ${presetsHeightPx}px + 1.2rem)`
|
||||
: `${locationHeightPx + presetsHeightPx}px`;
|
||||
dock.style.setProperty('--dock-location-pinned-height', `${locationHeightPx}px`);
|
||||
dock.style.setProperty('--dock-presets-pinned-height', `${presetsHeightPx}px`);
|
||||
dock.style.setProperty('--dock-lower-pinned-height', `${Math.ceil(lowerPinnedHeight)}px`);
|
||||
dock.style.setProperty('--dock-pinned-stack-height', stackHeight);
|
||||
dock.classList.toggle('dock-has-pinned-tray', pinnedCount > 0);
|
||||
dock.classList.toggle('dock-has-two-pinned-trays', pinnedCount > 1);
|
||||
}
|
||||
|
||||
_initRightPanelAdaptiveLayout() {
|
||||
if (this.destroyed) return;
|
||||
const stack = this._rightPanelStack;
|
||||
if (!stack || !this._ppToggles) return;
|
||||
|
||||
this._ppToggles.style.removeProperty('top');
|
||||
this._ppToggles.style.removeProperty('right');
|
||||
this._ppToggles.style.removeProperty('bottom');
|
||||
this._ppToggles.style.removeProperty('left');
|
||||
this._ppToggles.style.removeProperty('z-index');
|
||||
this._ppToggles.classList.remove('panel-draggable', 'panel-dragging');
|
||||
this._ppToggles.querySelector('.pp-header-row')?.removeAttribute('title');
|
||||
stack.prepend(this._ppToggles);
|
||||
const globalContextPanel = document.getElementById('global-context-panel');
|
||||
if (this._cctvPanel) {
|
||||
this._cctvPanel.style.removeProperty('top');
|
||||
this._cctvPanel.style.removeProperty('right');
|
||||
this._cctvPanel.style.removeProperty('bottom');
|
||||
this._cctvPanel.style.removeProperty('left');
|
||||
this._cctvPanel.style.removeProperty('z-index');
|
||||
this._cctvPanel.classList.remove('panel-draggable', 'panel-dragging');
|
||||
stack.insertBefore(this._cctvPanel, globalContextPanel);
|
||||
this._syncPanelCollapseButton(this._cctvPanel);
|
||||
}
|
||||
if (this._sliderPanel) {
|
||||
this._sliderPanel.style.removeProperty('top');
|
||||
this._sliderPanel.style.removeProperty('right');
|
||||
this._sliderPanel.style.removeProperty('bottom');
|
||||
this._sliderPanel.style.removeProperty('left');
|
||||
this._sliderPanel.style.removeProperty('max-height');
|
||||
const detectionGroup = this._detectionBtn?.closest('.pp-toggle-group');
|
||||
if (detectionGroup) detectionGroup.after(this._sliderPanel);
|
||||
else this._ppToggles.append(this._sliderPanel);
|
||||
}
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this._rightStackResizeObserver = new ResizeObserver(() => {
|
||||
this._scheduleRightPanelLayout();
|
||||
});
|
||||
this._rightStackResizeObserver.observe(stack);
|
||||
for (const panel of [this._ppToggles, this._cctvPanel, globalContextPanel]) {
|
||||
if (panel) this._rightStackResizeObserver.observe(panel);
|
||||
}
|
||||
document.querySelectorAll(RIGHT_STACK_OBSTACLE_SELECTOR).forEach((element) => {
|
||||
this._rightStackResizeObserver.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
this._rightStackMutationObserver = new MutationObserver(() => {
|
||||
this._scheduleRightPanelLayout();
|
||||
});
|
||||
this._rightStackMutationObserver.observe(stack, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'hidden', 'data-variant'],
|
||||
});
|
||||
const hud = document.getElementById('intel-hud');
|
||||
if (hud) {
|
||||
this._rightStackMutationObserver.observe(hud, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'hidden', 'data-variant'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const transitionHud = document.getElementById('intel-hud');
|
||||
if (transitionHud) {
|
||||
this._rightStackHudTransitionHandler = (event) => {
|
||||
if (event.propertyName === 'opacity' || event.propertyName === 'visibility') {
|
||||
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
|
||||
}
|
||||
};
|
||||
transitionHud.addEventListener('transitionend', this._rightStackHudTransitionHandler);
|
||||
}
|
||||
|
||||
this._scheduleRightPanelLayout();
|
||||
}
|
||||
|
||||
_scheduleRightPanelLayout({ reconsiderAutoCollapse = false } = {}) {
|
||||
if (this.destroyed) return;
|
||||
if (reconsiderAutoCollapse) this._rightStackReconsiderAutoCollapse = true;
|
||||
if (!this._rightPanelStack || this._rightStackLayoutFrame !== null) return;
|
||||
this._rightStackLayoutFrame = requestAnimationFrame(() => {
|
||||
if (this.destroyed) return;
|
||||
this._rightStackLayoutFrame = null;
|
||||
if (this._rightStackReconsiderAutoCollapse) {
|
||||
this._rightStackReconsiderAutoCollapse = false;
|
||||
for (const panel of this._rightPanelStack.querySelectorAll('.layout-auto-collapsed')) {
|
||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
||||
this._syncPanelCollapseButton(panel);
|
||||
}
|
||||
}
|
||||
this._syncRightPanelAdaptiveLayout();
|
||||
});
|
||||
}
|
||||
|
||||
_syncRightPanelAdaptiveLayout() {
|
||||
if (this.destroyed) return;
|
||||
layoutRightPanelRail({
|
||||
stack: this._rightPanelStack,
|
||||
obstacles: document.querySelectorAll(RIGHT_STACK_OBSTACLE_SELECTOR),
|
||||
windowRef: window,
|
||||
hud: this.readHud(),
|
||||
preferredPanelId: this._rightStackPreferredPanelId,
|
||||
onCollapse: (panel) => this._syncPanelCollapseButton(panel),
|
||||
onRetry: () => this._scheduleRightPanelLayout(),
|
||||
leftStack: this._leftPanelStack,
|
||||
displayPanel: this._ppToggles,
|
||||
readDisplayScrollTop: this.readDisplayScrollTop,
|
||||
});
|
||||
}
|
||||
|
||||
_initLeftPanelAdaptiveLayout() {
|
||||
if (this.destroyed) return;
|
||||
const stack = this._leftPanelStack;
|
||||
if (!stack) return;
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this._leftStackResizeObserver = new ResizeObserver(() => {
|
||||
this._scheduleLeftPanelLayout();
|
||||
});
|
||||
this._leftStackResizeObserver.observe(stack);
|
||||
stack.querySelectorAll(':scope > [data-panel-id]').forEach((panel) => {
|
||||
this._leftStackResizeObserver.observe(panel);
|
||||
const inner = [...panel.children].find((child) => !child.classList.contains('panel-glow'));
|
||||
if (inner) this._leftStackResizeObserver.observe(inner);
|
||||
});
|
||||
document.querySelectorAll(LEFT_STACK_OBSTACLE_SELECTOR).forEach((element) => {
|
||||
this._leftStackResizeObserver.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
this._leftStackMutationObserver = new MutationObserver(() => {
|
||||
this._scheduleLeftPanelLayout();
|
||||
});
|
||||
this._leftStackMutationObserver.observe(stack, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
const hud = document.getElementById('intel-hud');
|
||||
if (hud) {
|
||||
this._leftStackMutationObserver.observe(hud, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-variant'],
|
||||
});
|
||||
}
|
||||
const credits = document.getElementById('cesium-credits');
|
||||
if (credits) {
|
||||
this._leftStackMutationObserver.observe(credits, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const transitionHud = document.getElementById('intel-hud');
|
||||
if (transitionHud) {
|
||||
this._leftStackHudTransitionHandler = (event) => {
|
||||
if (event.propertyName === 'opacity' || event.propertyName === 'visibility') {
|
||||
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
|
||||
// The Cockpit strip hangs off the HUD's REC readout, so it has to
|
||||
// remeasure on the same event: the readout keeps its rect through
|
||||
// the whole fade and only stops counting once the HUD has retired.
|
||||
this.scheduleCockpitLayout();
|
||||
}
|
||||
};
|
||||
transitionHud.addEventListener('transitionend', this._leftStackHudTransitionHandler);
|
||||
}
|
||||
|
||||
this._leftStackCockpitModeHandler = () => {
|
||||
if (this.destroyed) return;
|
||||
// Cockpit mode repositions the peripheral HUD and reveals its own
|
||||
// bottom-left context card. Measure after those styles have committed so
|
||||
// the accordion remains in the same obstacle-safe lane instead of
|
||||
// jumping to a cockpit-specific top anchor.
|
||||
this._scheduleLeftPanelLayout();
|
||||
if (this._cockpitLayoutFrame !== null) cancelAnimationFrame(this._cockpitLayoutFrame);
|
||||
this._cockpitLayoutFrame = requestAnimationFrame(() => { this._cockpitLayoutFrame = null; this._scheduleLeftPanelLayout(); });
|
||||
clearTimeout(this._cockpitLayoutTimer);
|
||||
this._cockpitLayoutTimer = setTimeout(() => { this._cockpitLayoutTimer = null; this._scheduleLeftPanelLayout(); }, 300);
|
||||
};
|
||||
window.addEventListener('gev:cockpit-mode-changed', this._leftStackCockpitModeHandler);
|
||||
|
||||
this._scheduleLeftPanelLayout();
|
||||
}
|
||||
|
||||
_scheduleLeftPanelLayout({ reconsiderAutoCollapse = false } = {}) {
|
||||
if (this.destroyed) return;
|
||||
if (reconsiderAutoCollapse) this._leftStackReconsiderAutoCollapse = true;
|
||||
if (!this._leftPanelStack || this._leftStackLayoutFrame !== null) return;
|
||||
this._leftStackLayoutFrame = requestAnimationFrame(() => {
|
||||
if (this.destroyed) return;
|
||||
this._leftStackLayoutFrame = null;
|
||||
if (this._leftStackReconsiderAutoCollapse) {
|
||||
this._leftStackReconsiderAutoCollapse = false;
|
||||
for (const panel of this._leftPanelStack.querySelectorAll('.layout-auto-collapsed')) {
|
||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
||||
this._syncPanelCollapseButton(panel);
|
||||
}
|
||||
}
|
||||
this._syncLeftPanelAdaptiveLayout();
|
||||
});
|
||||
}
|
||||
|
||||
_syncLeftPanelAdaptiveLayout() {
|
||||
if (this.destroyed) return;
|
||||
layoutLeftPanelRail({
|
||||
stack: this._leftPanelStack,
|
||||
obstacles: document.querySelectorAll(LEFT_STACK_OBSTACLE_SELECTOR),
|
||||
windowRef: window,
|
||||
hud: this.readHud(),
|
||||
preferredPanelId: this._leftStackPreferredPanelId,
|
||||
onCollapse: (panel) => this._syncPanelCollapseButton(panel),
|
||||
onRetry: () => this._scheduleLeftPanelLayout(),
|
||||
collapsedHeights: this._leftStackCollapsedHeights,
|
||||
onAligned: () => this._scheduleRightPanelLayout(),
|
||||
});
|
||||
}
|
||||
destroy() {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
for (const field of ['_leftStackLayoutFrame', '_rightStackLayoutFrame', '_cockpitLayoutFrame']) {
|
||||
if (this[field] !== null) cancelAnimationFrame(this[field]);
|
||||
this[field] = null;
|
||||
}
|
||||
clearTimeout(this._adaptivePanelSettleTimer);
|
||||
clearTimeout(this._cockpitLayoutTimer);
|
||||
this._adaptivePanelSettleTimer = this._cockpitLayoutTimer = null;
|
||||
for (const field of ['_commandDockTrayObserver', '_leftStackResizeObserver', '_leftStackMutationObserver', '_rightStackResizeObserver', '_rightStackMutationObserver']) {
|
||||
this[field]?.disconnect(); this[field] = null;
|
||||
}
|
||||
for (const field of ['_leftStackHudTransitionHandler', '_rightStackHudTransitionHandler']) {
|
||||
if (this[field]) document.getElementById('intel-hud')?.removeEventListener('transitionend', this[field]);
|
||||
this[field] = null;
|
||||
}
|
||||
if (this._leftStackCockpitModeHandler) window.removeEventListener('gev:cockpit-mode-changed', this._leftStackCockpitModeHandler);
|
||||
this._leftStackCockpitModeHandler = null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user