FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
InspectorControls.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 React from 'react';
5import {
6 Accordion,
7 AccordionSummary,
8 AccordionDetails,
9 Typography,
10 TextField,
11 Box,
12} from '@mui/material';
13import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
14import { open } from '@tauri-apps/plugin-dialog';
15
16export const Section = ({
17 title,
18 children,
19}: {
20 title: string;
21 children: React.ReactNode;
22}) => (
23 <Accordion
24 defaultExpanded
25 disableGutters
26 elevation={0}
27 sx={{
28 border: 1,
29 borderColor: 'divider',
30 borderRadius: 1,
31 '&:before': {
32 display: 'none',
33 },
34 overflow: 'hidden',
35 }}
36 >
37 <AccordionSummary
38 expandIcon={<ExpandMoreIcon />}
39 sx={{
40 bgcolor: 'action.hover',
41 px: 2,
42 minHeight: 48,
43 '& .MuiAccordionSummary-content': {
44 my: 0,
45 },
46 '& .MuiAccordionSummary-expandIconWrapper': {
47 color: 'text.secondary',
48 },
49 }}
50 >
51 <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
52 {title}
53 </Typography>
54 </AccordionSummary>
55 <AccordionDetails
56 sx={{
57 p: 2,
58 display: 'flex',
59 flexDirection: 'column',
60 gap: 2,
61 borderTop: 1,
62 borderColor: 'divider',
63 }}
64 >
65 {children}
66 </AccordionDetails>
67 </Accordion>
68);
69
70export const NumberField = ({
71 label,
72 value,
73 onChange,
74}: {
75 label: string;
76 value: number | null;
77 onChange: (val: number | null) => void;
78}) => {
79 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
80 const strValue = e.target.value;
81
82 // If the field is cleared, represent it as null.
83 if (strValue === '') {
84 onChange(null);
85 return;
86 }
87
88 const numValue = parseFloat(strValue);
89
90 // Only update the parent state if the parsed value is a valid number.
91 // This prevents NaN from being stored and allows the user to type
92 // intermediate invalid states (e.g., "1.2.3", "-") without
93 // corrupting the application state.
94 if (!isNaN(numValue)) {
95 onChange(numValue);
96 }
97 };
98
99 return (
100 <TextField
101 label={label}
102 type="number"
103 variant="outlined"
104 size="small"
105 fullWidth
106 value={value ?? ''}
107 onChange={handleChange}
108 // Allow floating point numbers in the number input's spinners.
109 inputProps={{ step: 'any' }}
110 />
111 );
112};
113
114export const FileInput = ({
115 label,
116 value,
117 onChange,
118 filters,
119}: {
120 label: string;
121 value?: string;
122 onChange: (val: string) => void;
123 filters: { name: string; extensions: string[] }[];
124}) => {
125 const handleOpenFile = async () => {
126 try {
127 const selected = await open({
128 multiple: false,
129 filters,
130 });
131 if (typeof selected === 'string') {
132 onChange(selected);
133 }
134 } catch (err) {
135 console.error('Error opening file dialog:', err);
136 }
137 };
138
139 return (
140 <Box>
141 <Typography variant="caption" color="text.secondary">
142 {label}
143 </Typography>
144 <TextField
145 variant="outlined"
146 size="small"
147 fullWidth
148 value={value?.split(/[/\\]/).pop() ?? 'No file selected.'}
149 onClick={handleOpenFile}
150 sx={{ cursor: 'pointer' }}
151 slotProps={{
152 input: {
153 readOnly: true,
154 style: { cursor: 'pointer' },
155 },
156 }}
157 />
158 </Box>
159 );
160};