FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
MainLayout.tsx
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import { Alert, Box, Snackbar } from '@mui/material';
5import { listen } from '@tauri-apps/api/event';
6import React, { useEffect, useState } from 'react';
7import AboutDialog from '@/components/AboutDialog';
8import AppRail from '@/components/AppRail';
9import LicensesDialog from '@/components/LicensesDialog';
10import RawLogDrawer from '@/components/RawLogDrawer';
11import SettingsDialog from '@/components/SettingsDialog';
12import {
13 clampLogDrawerWidth,
14 type FersLogEntry,
15 getEffectiveLogDrawerWidth,
16 useFersLogStore,
17} from '@/stores/fersLogStore';
18import { useScenarioStore } from '@/stores/scenarioStore';
19import { AssetLibraryView } from '@/views/AssetLibraryView';
20import { ScenarioView } from '@/views/ScenarioView';
21import { SimulationView } from '@/views/SimulationView';
22import { Vita49StreamingView } from '@/views/Vita49StreamingView';
23
24export function MainLayout() {
25 const [activeView, setActiveView] = useState('scenario');
26 const [settingsOpen, setSettingsOpen] = useState(false);
27 const [aboutOpen, setAboutOpen] = useState(false);
28 const [licensesOpen, setLicensesOpen] = useState(false);
29 const [viewportWidth, setViewportWidth] = useState(() =>
30 typeof window === 'undefined' ? 1440 : window.innerWidth
31 );
32 const [liveLogDrawerWidth, setLiveLogDrawerWidth] = useState<number | null>(
33 null
34 );
35 const logOpen = useFersLogStore((state) => state.isOpen);
36 const appendLog = useFersLogStore((state) => state.appendLog);
37 const preferredLogDrawerWidth = useFersLogStore(
38 (state) => state.drawerWidth
39 );
40 const setPreferredLogDrawerWidth = useFersLogStore(
41 (state) => state.setDrawerWidth
42 );
43 const { open, message, severity } = useScenarioStore(
44 (state) => state.notificationSnackbar
45 );
46 const hideNotification = useScenarioStore(
47 (state) => state.hideNotification
48 );
49 const advanceNotification = useScenarioStore(
50 (state) => state.advanceNotification
51 );
52
53 useEffect(() => {
54 let active = true;
55 let unlistenLog: (() => void) | undefined;
56
57 listen<FersLogEntry>('fers-log', (event) => {
58 appendLog(event.payload);
59 }).then((unlisten) => {
60 if (active) {
61 unlistenLog = unlisten;
62 } else {
63 unlisten();
64 }
65 });
66
67 return () => {
68 active = false;
69 unlistenLog?.();
70 };
71 }, [appendLog]);
72
73 useEffect(() => {
74 const handleResize = () => {
75 setViewportWidth(window.innerWidth);
76 };
77
78 handleResize();
79 window.addEventListener('resize', handleResize);
80
81 return () => {
82 window.removeEventListener('resize', handleResize);
83 };
84 }, []);
85
86 useEffect(() => {
87 if (!logOpen) {
88 setLiveLogDrawerWidth(null);
89 }
90 }, [logOpen]);
91
92 const sidebarWidth = 60;
93 const requestedLogDrawerWidth =
94 liveLogDrawerWidth ?? preferredLogDrawerWidth;
95 const maxLogDrawerWidth = Math.max(0, viewportWidth - sidebarWidth);
96 const effectiveLogDrawerWidth = logOpen
97 ? getEffectiveLogDrawerWidth(requestedLogDrawerWidth, maxLogDrawerWidth)
98 : 0;
99
100 const handleLogDrawerResize = (nextWidth: number) => {
101 setLiveLogDrawerWidth(clampLogDrawerWidth(nextWidth));
102 };
103
104 const handleLogDrawerResizeEnd = (nextWidth: number) => {
105 const clampedWidth = clampLogDrawerWidth(nextWidth);
106 setLiveLogDrawerWidth(null);
107 setPreferredLogDrawerWidth(clampedWidth);
108 };
109
110 return (
111 <Box
112 sx={{
113 display: 'flex',
114 height: '100vh',
115 width: '100vw',
116 overflow: 'hidden',
117 position: 'fixed', // Ensure it stays in viewport
118 top: 0,
119 left: 0,
120 bgcolor: 'background.default',
121 }}
122 >
123 <AppRail
124 activeView={activeView}
125 onViewChange={setActiveView}
126 onSettingsClick={() => setSettingsOpen(true)}
127 onAboutClick={() => setAboutOpen(true)}
128 />
129 {logOpen && effectiveLogDrawerWidth > 0 && (
130 <RawLogDrawer
131 leftOffset={sidebarWidth}
132 width={effectiveLogDrawerWidth}
133 onResize={handleLogDrawerResize}
134 onResizeEnd={handleLogDrawerResizeEnd}
135 />
136 )}
137 <Box
138 component="main"
139 sx={{
140 flexGrow: 1,
141 minWidth: 0, // Allow shrinking below content size
142 height: '100%',
143 overflow: 'hidden', // Prevent overflow
144 position: 'relative',
145 bgcolor: 'background.default',
146 }}
147 >
148 {/* Render all views but only display the active one */}
149 <Box
150 sx={{
151 display: activeView === 'scenario' ? 'flex' : 'none',
152 height: '100%',
153 width: '100%',
154 }}
155 >
156 <ScenarioView isActive={activeView === 'scenario'} />
157 </Box>
158 <Box
159 sx={{
160 display: activeView === 'assets' ? 'block' : 'none',
161 height: '100%',
162 width: '100%',
163 }}
164 >
165 <AssetLibraryView />
166 </Box>
167 <Box
168 sx={{
169 display: activeView === 'simulation' ? 'block' : 'none',
170 height: '100%',
171 width: '100%',
172 }}
173 >
174 <SimulationView />
175 </Box>
176 <Box
177 sx={{
178 display: activeView === 'vita49' ? 'block' : 'none',
179 height: '100%',
180 width: '100%',
181 }}
182 >
183 <Vita49StreamingView />
184 </Box>
185 </Box>
186 <SettingsDialog
187 open={settingsOpen}
188 onClose={() => setSettingsOpen(false)}
189 />
190 <AboutDialog
191 open={aboutOpen}
192 onClose={() => setAboutOpen(false)}
193 onLicensesClick={() => {
194 setAboutOpen(false);
195 setLicensesOpen(true);
196 }}
197 />
198 <LicensesDialog
199 open={licensesOpen}
200 onClose={() => setLicensesOpen(false)}
201 />
202 <Snackbar
203 open={open}
204 autoHideDuration={6000}
205 onClose={hideNotification}
206 anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
207 TransitionProps={{ onExited: advanceNotification }}
208 >
209 <Alert
210 onClose={hideNotification}
211 severity={severity}
212 variant="filled"
213 sx={{ width: '100%' }}
214 >
215 {message}
216 </Alert>
217 </Snackbar>
218 </Box>
219 );
220}