mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
**Introduce Neumorphic UI design with new themes and styles**
- Added NeumorphicTheme implementation for light and dark modes. - Refactored `LayerSelector` and `MapNext` components to use the neumorphic style and color utilities. - Updated `menu.rs` with neumorphic-inspired button and background styling. - Enhanced GPS feed and vessel popups with neumorphic visuals, improving clarity and aesthetics. - Temporarily disabled base-map dependency in `yachtpit` for isolation testing.
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -9425,7 +9425,6 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
|||||||
name = "yachtpit"
|
name = "yachtpit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base-map",
|
|
||||||
"bevy",
|
"bevy",
|
||||||
"bevy_asset_loader",
|
"bevy_asset_loader",
|
||||||
"bevy_flurx",
|
"bevy_flurx",
|
||||||
|
@@ -1,18 +1,15 @@
|
|||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
import {Box, Button, HStack, Input} from '@chakra-ui/react';
|
import {Box, Button, HStack, Input, Text} from '@chakra-ui/react';
|
||||||
|
import { useColorMode } from './components/ui/color-mode';
|
||||||
import {useCallback, useEffect, useState} from "react";
|
import {useCallback, useEffect, useState} from "react";
|
||||||
import MapNext, {type Geolocation} from "@/MapNext.tsx";
|
import MapNext, {type Geolocation} from "@/MapNext.tsx";
|
||||||
|
import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme';
|
||||||
|
import {layers, LayerSelector} from "@/LayerSelector.tsx";
|
||||||
|
|
||||||
// public key
|
// public key
|
||||||
const key =
|
const key =
|
||||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||||
|
|
||||||
const layers = [
|
|
||||||
{ name: 'OSM', value: 'mapbox://styles/mapbox/dark-v11' },
|
|
||||||
{ name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const vesselLayerStyle: CircleLayerSpecification = {
|
// const vesselLayerStyle: CircleLayerSpecification = {
|
||||||
// id: 'vessel',
|
// id: 'vessel',
|
||||||
@@ -40,8 +37,6 @@ interface VesselStatus {
|
|||||||
speed: number;
|
speed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Layer = { name: string; value: string };
|
|
||||||
export type Layers = Layer[];
|
|
||||||
|
|
||||||
class MyGeolocation implements Geolocation {
|
class MyGeolocation implements Geolocation {
|
||||||
constructor({clearWatch, getCurrentPosition, watchPosition}: {
|
constructor({clearWatch, getCurrentPosition, watchPosition}: {
|
||||||
@@ -65,6 +60,155 @@ class MyGeolocation implements Geolocation {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const custom_geolocation = new MyGeolocation({
|
||||||
|
clearWatch: (watchId: number) => {
|
||||||
|
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
||||||
|
const interval = (window as any).geolocationWatches.get(watchId);
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
(window as any).geolocationWatches.delete(watchId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => {
|
||||||
|
if (typeof window === 'undefined') return 0;
|
||||||
|
|
||||||
|
// Initialize watches map if it doesn't exist
|
||||||
|
if (!(window as any).geolocationWatches) {
|
||||||
|
(window as any).geolocationWatches = new Map();
|
||||||
|
}
|
||||||
|
if (!(window as any).geolocationWatchId) {
|
||||||
|
(window as any).geolocationWatchId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchId = ++(window as any).geolocationWatchId;
|
||||||
|
|
||||||
|
const pollPosition = async () => {
|
||||||
|
if ((window as any).__FLURX__) {
|
||||||
|
try {
|
||||||
|
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||||
|
const position: GeolocationPosition = {
|
||||||
|
coords: {
|
||||||
|
latitude: vesselStatus.latitude,
|
||||||
|
longitude: vesselStatus.longitude,
|
||||||
|
altitude: null,
|
||||||
|
accuracy: 10, // Assume 10m accuracy
|
||||||
|
altitudeAccuracy: null,
|
||||||
|
heading: vesselStatus.heading,
|
||||||
|
speed: vesselStatus.speed,
|
||||||
|
toJSON: () => ({
|
||||||
|
latitude: vesselStatus.latitude,
|
||||||
|
longitude: vesselStatus.longitude,
|
||||||
|
altitude: null,
|
||||||
|
accuracy: 10,
|
||||||
|
altitudeAccuracy: null,
|
||||||
|
heading: vesselStatus.heading,
|
||||||
|
speed: vesselStatus.speed
|
||||||
|
})
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
toJSON: () => ({
|
||||||
|
coords: {
|
||||||
|
latitude: vesselStatus.latitude,
|
||||||
|
longitude: vesselStatus.longitude,
|
||||||
|
altitude: null,
|
||||||
|
accuracy: 10,
|
||||||
|
altitudeAccuracy: null,
|
||||||
|
heading: vesselStatus.heading,
|
||||||
|
speed: vesselStatus.speed
|
||||||
|
},
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
successCallback(position);
|
||||||
|
} catch (error) {
|
||||||
|
if (errorCallback) {
|
||||||
|
const positionError: GeolocationPositionError = {
|
||||||
|
code: 2, // POSITION_UNAVAILABLE
|
||||||
|
message: 'Failed to get vessel status: ' + error,
|
||||||
|
PERMISSION_DENIED: 1,
|
||||||
|
POSITION_UNAVAILABLE: 2,
|
||||||
|
TIMEOUT: 3
|
||||||
|
};
|
||||||
|
errorCallback(positionError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Poll immediately and then at intervals
|
||||||
|
pollPosition();
|
||||||
|
const interval = setInterval(pollPosition, options?.timeout || 5000);
|
||||||
|
(window as any).geolocationWatches.set(watchId, interval);
|
||||||
|
|
||||||
|
return watchId;
|
||||||
|
},
|
||||||
|
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => {
|
||||||
|
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||||
|
const position: GeolocationPosition = {
|
||||||
|
coords: {
|
||||||
|
latitude: vesselStatus.latitude,
|
||||||
|
longitude: vesselStatus.longitude,
|
||||||
|
altitude: null,
|
||||||
|
accuracy: 10, // Assume 10m accuracy
|
||||||
|
altitudeAccuracy: null,
|
||||||
|
heading: vesselStatus.heading,
|
||||||
|
speed: vesselStatus.speed,
|
||||||
|
toJSON: () => ({
|
||||||
|
latitude: vesselStatus.latitude,
|
||||||
|
longitude: vesselStatus.longitude,
|
||||||
|
altitude: null,
|
||||||
|
accuracy: 10,
|
||||||
|
altitudeAccuracy: null,
|
||||||
|
heading: vesselStatus.heading,
|
||||||
|
speed: vesselStatus.speed
|
||||||
|
})
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
toJSON: () => ({
|
||||||
|
coords: {
|
||||||
|
latitude: vesselStatus.latitude,
|
||||||
|
longitude: vesselStatus.longitude,
|
||||||
|
altitude: null,
|
||||||
|
accuracy: 10,
|
||||||
|
altitudeAccuracy: null,
|
||||||
|
heading: vesselStatus.heading,
|
||||||
|
speed: vesselStatus.speed
|
||||||
|
},
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
successCallback(position);
|
||||||
|
} catch (error) {
|
||||||
|
if (errorCallback) {
|
||||||
|
const positionError: GeolocationPositionError = {
|
||||||
|
code: 2, // POSITION_UNAVAILABLE
|
||||||
|
message: 'Failed to get vessel status: ' + error,
|
||||||
|
PERMISSION_DENIED: 1,
|
||||||
|
POSITION_UNAVAILABLE: 2,
|
||||||
|
TIMEOUT: 3
|
||||||
|
};
|
||||||
|
errorCallback(positionError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} else if (errorCallback) {
|
||||||
|
const positionError: GeolocationPositionError = {
|
||||||
|
code: 2, // POSITION_UNAVAILABLE
|
||||||
|
message: '__FLURX__ not available',
|
||||||
|
PERMISSION_DENIED: 1,
|
||||||
|
POSITION_UNAVAILABLE: 2,
|
||||||
|
TIMEOUT: 3
|
||||||
|
};
|
||||||
|
errorCallback(positionError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// interface MapViewParams {
|
// interface MapViewParams {
|
||||||
// latitude: number;
|
// latitude: number;
|
||||||
// longitude: number;
|
// longitude: number;
|
||||||
@@ -76,49 +220,10 @@ class MyGeolocation implements Geolocation {
|
|||||||
// token: string | null;
|
// token: string | null;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function LayerSelector(props: { onClick: (e: any) => Promise<void> }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box position="relative">
|
|
||||||
<Button colorScheme="blue" size="sm" variant="solid" onClick={() => setIsOpen(!isOpen)}>
|
|
||||||
Layer
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="100%"
|
|
||||||
left={0}
|
|
||||||
w="200px"
|
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
|
||||||
boxShadow="md"
|
|
||||||
zIndex={2}
|
|
||||||
>
|
|
||||||
{layers.map(layer => (
|
|
||||||
<Box
|
|
||||||
key={layer.value}
|
|
||||||
id={layer.value}
|
|
||||||
p={2}
|
|
||||||
cursor="pointer"
|
|
||||||
color="white"
|
|
||||||
_hover={{ bg: 'whiteAlpha.200' }}
|
|
||||||
onClick={async e => {
|
|
||||||
setIsOpen(false);
|
|
||||||
await props.onClick(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{layer.name}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||||
const [searchInput, setSearchInput] = useState('');
|
const [searchInput, setSearchInput] = useState('');
|
||||||
@@ -215,12 +320,10 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [isSearchOpen, searchInput]);
|
}, [isSearchOpen, searchInput]);
|
||||||
|
|
||||||
const handleLayerChange = useCallback(async (e: any) => {
|
const handleLayerChange = useCallback(async (layer: any) => {
|
||||||
const newLayer = layers.find(layer => layer.value === e.target.id);
|
console.log('Layer change requested:', layer);
|
||||||
if (newLayer) {
|
setSelectedLayer(layer);
|
||||||
setSelectedLayer(newLayer);
|
console.log('Layer changed to:', layer.name);
|
||||||
console.log('Layer changed to:', newLayer.name);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const handleMapViewChange = useCallback(async (evt: any) => {
|
// const handleMapViewChange = useCallback(async (evt: any) => {
|
||||||
@@ -289,25 +392,24 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
||||||
<Box w="100vw" h="100vh" position="relative" overflow="hidden">
|
<Box w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||||
{/* GPS Feed Display — absolutely positioned at bottom-left */}
|
{/* GPS Feed Display — absolutely positioned at top-right */}
|
||||||
{vesselPosition && (
|
{vesselPosition && (
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={65}
|
top={65}
|
||||||
right={4}
|
right={4}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
p={4}
|
||||||
color="white"
|
|
||||||
p={3}
|
|
||||||
borderRadius="md"
|
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
fontFamily="monospace"
|
fontFamily="monospace"
|
||||||
minW="200px"
|
minW="220px"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||||
>
|
>
|
||||||
<Box fontWeight="bold" mb={2}>GPS Feed</Box>
|
<Box fontWeight="bold" mb={3} fontSize="md">GPS Feed</Box>
|
||||||
<Box>Lat: {vesselPosition.latitude.toFixed(6)}°</Box>
|
<Box mb={1}>Lat: {vesselPosition.latitude.toFixed(6)}°</Box>
|
||||||
<Box>Lon: {vesselPosition.longitude.toFixed(6)}°</Box>
|
<Box mb={1}>Lon: {vesselPosition.longitude.toFixed(6)}°</Box>
|
||||||
<Box>Heading: {vesselPosition.heading.toFixed(1)}°</Box>
|
<Box mb={1}>Heading: {vesselPosition.heading.toFixed(1)}°</Box>
|
||||||
<Box>Speed: {vesselPosition.speed.toFixed(1)} kts</Box>
|
<Box>Speed: {vesselPosition.speed.toFixed(1)} kts</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -316,39 +418,31 @@ function App() {
|
|||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
position="relative"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
colorScheme="teal"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="solid"
|
variant="surface"
|
||||||
onClick={handleSearchClick}
|
onClick={handleSearchClick}
|
||||||
mr={2}
|
mr={2}
|
||||||
|
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||||
>
|
>
|
||||||
Search
|
<Text>SEARCH</Text>
|
||||||
</Button>
|
</Button>
|
||||||
{isSearchOpen && <Box
|
{isSearchOpen && <Box
|
||||||
w="200px"
|
w="200px"
|
||||||
transition="all 0.3s"
|
|
||||||
transform={`translateX(${isSearchOpen ? "0" : "100%"})`}
|
transform={`translateX(${isSearchOpen ? "0" : "100%"})`}
|
||||||
background="rgba(0, 0, 0, 0.8)"
|
|
||||||
opacity={isSearchOpen ? 1 : 0}
|
opacity={isSearchOpen ? 1 : 0}
|
||||||
color="white"
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
size="sm"
|
size="sm"
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={e => setSearchInput(e.target.value)}
|
onChange={e => setSearchInput(e.target.value)}
|
||||||
color="white"
|
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
|
||||||
border="none"
|
border="none"
|
||||||
borderRadius="0"
|
{...getNeumorphicStyle(colorMode as 'light' | 'dark', 'pressed')}
|
||||||
_focus={{
|
|
||||||
outline: 'none',
|
|
||||||
}}
|
|
||||||
_placeholder={{
|
|
||||||
color: "#d1cfcf"
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{searchResults.length > 0 && (
|
{searchResults.length > 0 && (
|
||||||
<Box
|
<Box
|
||||||
@@ -356,180 +450,42 @@ function App() {
|
|||||||
top="100%"
|
top="100%"
|
||||||
left={0}
|
left={0}
|
||||||
w="200px"
|
w="200px"
|
||||||
bg="rgba(0, 0, 0, 0.8)"
|
|
||||||
boxShadow="md"
|
|
||||||
zIndex={2}
|
zIndex={2}
|
||||||
|
mt={2}
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||||
>
|
>
|
||||||
{searchResults.map((result, index) => (
|
{searchResults.map((result, index) => {
|
||||||
<Box
|
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
||||||
key={index}
|
return (
|
||||||
p={2}
|
<Box
|
||||||
cursor="pointer"
|
key={index}
|
||||||
color="white"
|
p={3}
|
||||||
_hover={{ bg: 'whiteAlpha.200' }}
|
cursor="pointer"
|
||||||
onClick={async () => {
|
borderRadius="8px"
|
||||||
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
transition="all 0.2s ease-in-out"
|
||||||
await selectSearchResult(result);
|
_hover={{
|
||||||
setSearchResults([]);
|
bg: colors.accent + '20',
|
||||||
setIsSearchOpen(false);
|
transform: 'translateY(-1px)',
|
||||||
}}
|
}}
|
||||||
>
|
onClick={async () => {
|
||||||
{`${result.lat}, ${result.lon}`}
|
console.log(`Selecting result ${result.lat}, ${result.lon}`);
|
||||||
</Box>
|
await selectSearchResult(result);
|
||||||
))}
|
setSearchResults([]);
|
||||||
|
setIsSearchOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${result.lat}, ${result.lon}`}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>}
|
</Box>}
|
||||||
</Box>
|
</Box>
|
||||||
<LayerSelector onClick={handleLayerChange} />
|
<LayerSelector onClick={handleLayerChange} />
|
||||||
</HStack>
|
</HStack>
|
||||||
<MapNext mapboxPublicKey={atob(key)} vesselPosition={vesselPosition} layer={selectedLayer} mapView={mapView} geolocation={new MyGeolocation({
|
<MapNext mapboxPublicKey={atob(key)} vesselPosition={vesselPosition} layer={selectedLayer} mapView={mapView} geolocation={window.navigator.geolocation || custom_geolocation}/>
|
||||||
clearWatch: (watchId: number) => {
|
|
||||||
if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
|
|
||||||
const interval = (window as any).geolocationWatches.get(watchId);
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
(window as any).geolocationWatches.delete(watchId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => {
|
|
||||||
if (typeof window === 'undefined') return 0;
|
|
||||||
|
|
||||||
// Initialize watches map if it doesn't exist
|
|
||||||
if (!(window as any).geolocationWatches) {
|
|
||||||
(window as any).geolocationWatches = new Map();
|
|
||||||
}
|
|
||||||
if (!(window as any).geolocationWatchId) {
|
|
||||||
(window as any).geolocationWatchId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const watchId = ++(window as any).geolocationWatchId;
|
|
||||||
|
|
||||||
const pollPosition = async () => {
|
|
||||||
if ((window as any).__FLURX__) {
|
|
||||||
try {
|
|
||||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
|
||||||
const position: GeolocationPosition = {
|
|
||||||
coords: {
|
|
||||||
latitude: vesselStatus.latitude,
|
|
||||||
longitude: vesselStatus.longitude,
|
|
||||||
altitude: null,
|
|
||||||
accuracy: 10, // Assume 10m accuracy
|
|
||||||
altitudeAccuracy: null,
|
|
||||||
heading: vesselStatus.heading,
|
|
||||||
speed: vesselStatus.speed,
|
|
||||||
toJSON: () => ({
|
|
||||||
latitude: vesselStatus.latitude,
|
|
||||||
longitude: vesselStatus.longitude,
|
|
||||||
altitude: null,
|
|
||||||
accuracy: 10,
|
|
||||||
altitudeAccuracy: null,
|
|
||||||
heading: vesselStatus.heading,
|
|
||||||
speed: vesselStatus.speed
|
|
||||||
})
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
toJSON: () => ({
|
|
||||||
coords: {
|
|
||||||
latitude: vesselStatus.latitude,
|
|
||||||
longitude: vesselStatus.longitude,
|
|
||||||
altitude: null,
|
|
||||||
accuracy: 10,
|
|
||||||
altitudeAccuracy: null,
|
|
||||||
heading: vesselStatus.heading,
|
|
||||||
speed: vesselStatus.speed
|
|
||||||
},
|
|
||||||
timestamp: Date.now()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
successCallback(position);
|
|
||||||
} catch (error) {
|
|
||||||
if (errorCallback) {
|
|
||||||
const positionError: GeolocationPositionError = {
|
|
||||||
code: 2, // POSITION_UNAVAILABLE
|
|
||||||
message: 'Failed to get vessel status: ' + error,
|
|
||||||
PERMISSION_DENIED: 1,
|
|
||||||
POSITION_UNAVAILABLE: 2,
|
|
||||||
TIMEOUT: 3
|
|
||||||
};
|
|
||||||
errorCallback(positionError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Poll immediately and then at intervals
|
|
||||||
pollPosition();
|
|
||||||
const interval = setInterval(pollPosition, options?.timeout || 5000);
|
|
||||||
(window as any).geolocationWatches.set(watchId, interval);
|
|
||||||
|
|
||||||
return watchId;
|
|
||||||
},
|
|
||||||
getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => {
|
|
||||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
|
||||||
const position: GeolocationPosition = {
|
|
||||||
coords: {
|
|
||||||
latitude: vesselStatus.latitude,
|
|
||||||
longitude: vesselStatus.longitude,
|
|
||||||
altitude: null,
|
|
||||||
accuracy: 10, // Assume 10m accuracy
|
|
||||||
altitudeAccuracy: null,
|
|
||||||
heading: vesselStatus.heading,
|
|
||||||
speed: vesselStatus.speed,
|
|
||||||
toJSON: () => ({
|
|
||||||
latitude: vesselStatus.latitude,
|
|
||||||
longitude: vesselStatus.longitude,
|
|
||||||
altitude: null,
|
|
||||||
accuracy: 10,
|
|
||||||
altitudeAccuracy: null,
|
|
||||||
heading: vesselStatus.heading,
|
|
||||||
speed: vesselStatus.speed
|
|
||||||
})
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
toJSON: () => ({
|
|
||||||
coords: {
|
|
||||||
latitude: vesselStatus.latitude,
|
|
||||||
longitude: vesselStatus.longitude,
|
|
||||||
altitude: null,
|
|
||||||
accuracy: 10,
|
|
||||||
altitudeAccuracy: null,
|
|
||||||
heading: vesselStatus.heading,
|
|
||||||
speed: vesselStatus.speed
|
|
||||||
},
|
|
||||||
timestamp: Date.now()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
successCallback(position);
|
|
||||||
} catch (error) {
|
|
||||||
if (errorCallback) {
|
|
||||||
const positionError: GeolocationPositionError = {
|
|
||||||
code: 2, // POSITION_UNAVAILABLE
|
|
||||||
message: 'Failed to get vessel status: ' + error,
|
|
||||||
PERMISSION_DENIED: 1,
|
|
||||||
POSITION_UNAVAILABLE: 2,
|
|
||||||
TIMEOUT: 3
|
|
||||||
};
|
|
||||||
errorCallback(positionError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} else if (errorCallback) {
|
|
||||||
const positionError: GeolocationPositionError = {
|
|
||||||
code: 2, // POSITION_UNAVAILABLE
|
|
||||||
message: '__FLURX__ not available',
|
|
||||||
PERMISSION_DENIED: 1,
|
|
||||||
POSITION_UNAVAILABLE: 2,
|
|
||||||
TIMEOUT: 3
|
|
||||||
};
|
|
||||||
errorCallback(positionError);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}/>
|
|
||||||
{/*<Map*/}
|
{/*<Map*/}
|
||||||
{/* mapboxAccessToken={atob(key)}*/}
|
{/* mapboxAccessToken={atob(key)}*/}
|
||||||
{/* initialViewState={mapView}*/}
|
{/* initialViewState={mapView}*/}
|
||||||
|
91
crates/base-map/map/src/LayerSelector.tsx
Normal file
91
crates/base-map/map/src/LayerSelector.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
import {Button, Menu, Portal} from '@chakra-ui/react';
|
||||||
|
import {useColorMode} from './components/ui/color-mode';
|
||||||
|
import {useState} from "react";
|
||||||
|
import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme';
|
||||||
|
|
||||||
|
export const layers = [
|
||||||
|
{ name: 'OSM', value: 'mapbox://styles/mapbox/dark-v11' },
|
||||||
|
{ name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const vesselLayerStyle: CircleLayerSpecification = {
|
||||||
|
// id: 'vessel',
|
||||||
|
// type: 'circle',
|
||||||
|
// paint: {
|
||||||
|
// 'circle-radius': 8,
|
||||||
|
// 'circle-color': '#ff4444',
|
||||||
|
// 'circle-stroke-width': 2,
|
||||||
|
// 'circle-stroke-color': '#ffffff'
|
||||||
|
// },
|
||||||
|
// source: ''
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
export type Layer = { name: string; value: string };
|
||||||
|
export type Layers = Layer[];
|
||||||
|
|
||||||
|
// interface MapViewParams {
|
||||||
|
// latitude: number;
|
||||||
|
// longitude: number;
|
||||||
|
// zoom: number;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface AuthParams {
|
||||||
|
// authenticated: boolean;
|
||||||
|
// token: string | null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function LayerSelector(props: { onClick: (layer: Layer) => Promise<void> }) {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||||
|
const neumorphicStyle = getNeumorphicStyle(colorMode as 'light' | 'dark');
|
||||||
|
const colors = getNeumorphicColors(colorMode as 'light' | 'dark');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="surface"
|
||||||
|
{...neumorphicStyle}
|
||||||
|
>
|
||||||
|
{selectedLayer?.name || 'Layer'}
|
||||||
|
</Button>
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Portal>
|
||||||
|
<Menu.Positioner>
|
||||||
|
<Menu.Content
|
||||||
|
minW="200px"
|
||||||
|
py={2}
|
||||||
|
{...neumorphicStyle}
|
||||||
|
>
|
||||||
|
{layers.map(layer => (
|
||||||
|
<Menu.Item
|
||||||
|
key={layer.value}
|
||||||
|
id={layer.value}
|
||||||
|
value={layer.value}
|
||||||
|
borderRadius={6}
|
||||||
|
transition="all 0.2s ease-in-out"
|
||||||
|
_hover={{
|
||||||
|
bg: colors.accent + '20',
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
// @ts-ignore
|
||||||
|
console.log(e.target.id)
|
||||||
|
setSelectedLayer(layer);
|
||||||
|
props.onClick(layer);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{layer.name}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Content>
|
||||||
|
</Menu.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</Menu.Root>
|
||||||
|
);
|
||||||
|
}
|
@@ -17,6 +17,8 @@ import { useRealAISProvider } from './real-ais-provider.tsx';
|
|||||||
|
|
||||||
import PORTS from './test_data/nautical-base-data.json';
|
import PORTS from './test_data/nautical-base-data.json';
|
||||||
import {Box} from "@chakra-ui/react";
|
import {Box} from "@chakra-ui/react";
|
||||||
|
import { useColorMode } from './components/ui/color-mode';
|
||||||
|
import { getNeumorphicStyle, getNeumorphicColors } from './theme/neumorphic-theme';
|
||||||
|
|
||||||
|
|
||||||
export interface Geolocation {
|
export interface Geolocation {
|
||||||
@@ -31,6 +33,7 @@ export interface Geolocation {
|
|||||||
|
|
||||||
|
|
||||||
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
export default function MapNext(props: any = {mapboxPublicKey: "", geolocation: Geolocation, vesselPosition: undefined, layer: undefined, mapView: undefined} as any) {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
const [popupInfo, setPopupInfo] = useState(null);
|
const [popupInfo, setPopupInfo] = useState(null);
|
||||||
const [vesselPopupInfo, setVesselPopupInfo] = useState<VesselData | null>(null);
|
const [vesselPopupInfo, setVesselPopupInfo] = useState<VesselData | null>(null);
|
||||||
const [boundingBox, setBoundingBox] = useState<{sw_lat: number, sw_lon: number, ne_lat: number, ne_lon: number} | undefined>(undefined);
|
const [boundingBox, setBoundingBox] = useState<{sw_lat: number, sw_lon: number, ne_lat: number, ne_lon: number} | undefined>(undefined);
|
||||||
@@ -230,28 +233,45 @@ export default function MapNext(props: any = {mapboxPublicKey: "", geolocation:
|
|||||||
latitude={vesselPopupInfo.latitude}
|
latitude={vesselPopupInfo.latitude}
|
||||||
onClose={() => setVesselPopupInfo(null)}
|
onClose={() => setVesselPopupInfo(null)}
|
||||||
>
|
>
|
||||||
<div style={{ minWidth: '200px' }}>
|
<Box
|
||||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: 'bold' }}>
|
minW="240px"
|
||||||
|
p={4}
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
{...getNeumorphicStyle(colorMode as 'light' | 'dark')}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
mb={3}
|
||||||
|
fontSize="lg"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={getNeumorphicColors(colorMode as 'light' | 'dark').text}
|
||||||
|
>
|
||||||
{vesselPopupInfo.name}
|
{vesselPopupInfo.name}
|
||||||
</h3>
|
</Box>
|
||||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
<Box fontSize="sm" lineHeight="1.6">
|
||||||
<div><strong>Type:</strong> {vesselPopupInfo.type}</div>
|
<Box mb={1}><strong>Type:</strong> {vesselPopupInfo.type}</Box>
|
||||||
<div><strong>MMSI:</strong> {vesselPopupInfo.mmsi}</div>
|
<Box mb={1}><strong>MMSI:</strong> {vesselPopupInfo.mmsi}</Box>
|
||||||
<div><strong>Call Sign:</strong> {vesselPopupInfo.callSign}</div>
|
<Box mb={1}><strong>Call Sign:</strong> {vesselPopupInfo.callSign}</Box>
|
||||||
<div><strong>Speed:</strong> {vesselPopupInfo.speed.toFixed(1)} knots</div>
|
<Box mb={1}><strong>Speed:</strong> {vesselPopupInfo.speed.toFixed(1)} knots</Box>
|
||||||
<div><strong>Heading:</strong> {vesselPopupInfo.heading.toFixed(0)}°</div>
|
<Box mb={1}><strong>Heading:</strong> {vesselPopupInfo.heading.toFixed(0)}°</Box>
|
||||||
<div><strong>Length:</strong> {vesselPopupInfo.length.toFixed(0)}m</div>
|
<Box mb={1}><strong>Length:</strong> {vesselPopupInfo.length.toFixed(0)}m</Box>
|
||||||
{vesselPopupInfo.destination && (
|
{vesselPopupInfo.destination && (
|
||||||
<div><strong>Destination:</strong> {vesselPopupInfo.destination}</div>
|
<Box mb={1}><strong>Destination:</strong> {vesselPopupInfo.destination}</Box>
|
||||||
)}
|
)}
|
||||||
{vesselPopupInfo.eta && (
|
{vesselPopupInfo.eta && (
|
||||||
<div><strong>ETA:</strong> {vesselPopupInfo.eta}</div>
|
<Box mb={1}><strong>ETA:</strong> {vesselPopupInfo.eta}</Box>
|
||||||
)}
|
)}
|
||||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
|
<Box
|
||||||
|
fontSize="xs"
|
||||||
|
color={getNeumorphicColors(colorMode as 'light' | 'dark').textSecondary}
|
||||||
|
mt={3}
|
||||||
|
pt={2}
|
||||||
|
borderTop="1px solid"
|
||||||
|
borderColor={getNeumorphicColors(colorMode as 'light' | 'dark').textSecondary + '30'}
|
||||||
|
>
|
||||||
Last Update: {vesselPopupInfo.lastUpdate.toLocaleTimeString()}
|
Last Update: {vesselPopupInfo.lastUpdate.toLocaleTimeString()}
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
</Popup>
|
</Popup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
75
crates/base-map/map/src/theme/neumorphic-theme.ts
Normal file
75
crates/base-map/map/src/theme/neumorphic-theme.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { defineConfig } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
// Neumorphic color palette
|
||||||
|
const neumorphicColors = {
|
||||||
|
light: {
|
||||||
|
bg: '#e0e5ec',
|
||||||
|
surface: '#e0e5ec',
|
||||||
|
text: '#2d3748',
|
||||||
|
textSecondary: '#4a5568',
|
||||||
|
accent: '#3182ce',
|
||||||
|
shadow: {
|
||||||
|
dark: '#a3b1c6',
|
||||||
|
light: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
bg: '#2d3748',
|
||||||
|
surface: '#ffffff',
|
||||||
|
text: '#f7fafc',
|
||||||
|
textSecondary: '#e2e8f0',
|
||||||
|
accent: '#63b3ed',
|
||||||
|
shadow: {
|
||||||
|
dark: '#1a202c',
|
||||||
|
light: '#4a5568',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Neumorphic shadow mixins
|
||||||
|
const neumorphicShadows = {
|
||||||
|
light: {
|
||||||
|
raised: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff',
|
||||||
|
pressed: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff',
|
||||||
|
subtle: '1px 1px 2px #a3b1c6, -1px -1px 2px #ffffff',
|
||||||
|
subtlePressed: 'inset 1px 1px 2px #a3b1c6, inset -1px -1px 2px #ffffff',
|
||||||
|
floating: '6px 6px 12px #a3b1c6, -6px -6px 12px #ffffff',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
raised: '2px 2px 4px #1a202c, -2px -2px 4px #4a5568',
|
||||||
|
pressed: 'inset 2px 2px 4px #1a202c, inset -2px -2px 4px #4a5568',
|
||||||
|
subtle: '2px 2px 2px #1a202c, -2px -2px 2px #4a5568',
|
||||||
|
subtlePressed: 'inset 2px 2px 2px #1a202c, inset -2px -2px 2px #4a5568',
|
||||||
|
floating: '6px 6px 12px #1a202c, -6px -6px 12px #4a5568',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplified theme configuration to avoid TypeScript errors
|
||||||
|
// The utility functions below provide the neumorphic styling functionality
|
||||||
|
export const neumorphicTheme = defineConfig({
|
||||||
|
theme: {
|
||||||
|
// Theme configuration simplified to avoid type errors
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility functions for neumorphic styling
|
||||||
|
export const getNeumorphicStyle = (colorMode: 'light' | 'dark', variant: 'raised' | 'pressed' | 'subtle' | 'floating' = 'raised') => {
|
||||||
|
const colors = neumorphicColors[colorMode];
|
||||||
|
const shadows = neumorphicShadows[colorMode];
|
||||||
|
|
||||||
|
return {
|
||||||
|
bg: colors.surface,
|
||||||
|
color: colors.text,
|
||||||
|
borderRadius: 6,
|
||||||
|
boxShadow: shadows[variant] || shadows.raised,
|
||||||
|
transition: 'all 0.3s ease-in-out',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNeumorphicColors = (colorMode: 'light' | 'dark') => {
|
||||||
|
return neumorphicColors[colorMode];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNeumorphicShadows = (colorMode: 'light' | 'dark') => {
|
||||||
|
return neumorphicShadows[colorMode];
|
||||||
|
};
|
@@ -5,7 +5,7 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['./src/test-setup.ts'],
|
setupFiles: ['./test/test-setup.ts'],
|
||||||
globals: true,
|
globals: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
@@ -100,4 +100,4 @@ console_error_panic_hook = "0.1"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
embed-resource = "1"
|
embed-resource = "1"
|
||||||
base-map = { path = "../base-map" }
|
# base-map = { path = "../base-map" } # Temporarily disabled for testing
|
||||||
|
@@ -13,17 +13,42 @@ impl Plugin for MenuPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component, Clone)]
|
||||||
struct ButtonColors {
|
struct ButtonColors {
|
||||||
normal: Color,
|
normal: Color,
|
||||||
hovered: Color,
|
hovered: Color,
|
||||||
|
pressed: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neumorphic color palette for luxury design
|
||||||
|
struct NeumorphicColors;
|
||||||
|
|
||||||
|
impl NeumorphicColors {
|
||||||
|
// Base surface color - soft gray with warm undertones
|
||||||
|
const SURFACE: Color = Color::linear_rgb(0.88, 0.90, 0.92);
|
||||||
|
|
||||||
|
// Primary button colors with depth
|
||||||
|
const PRIMARY_NORMAL: Color = Color::linear_rgb(0.85, 0.87, 0.90);
|
||||||
|
const PRIMARY_HOVERED: Color = Color::linear_rgb(0.90, 0.92, 0.95);
|
||||||
|
const PRIMARY_PRESSED: Color = Color::linear_rgb(0.80, 0.82, 0.85);
|
||||||
|
|
||||||
|
// Secondary button colors (more subtle)
|
||||||
|
const SECONDARY_NORMAL: Color = Color::linear_rgb(0.86, 0.88, 0.91);
|
||||||
|
const SECONDARY_HOVERED: Color = Color::linear_rgb(0.88, 0.90, 0.93);
|
||||||
|
const SECONDARY_PRESSED: Color = Color::linear_rgb(0.82, 0.84, 0.87);
|
||||||
|
|
||||||
|
// Text colors for contrast
|
||||||
|
const TEXT_PRIMARY: Color = Color::linear_rgb(0.25, 0.30, 0.35);
|
||||||
|
const TEXT_SECONDARY: Color = Color::linear_rgb(0.45, 0.50, 0.55);
|
||||||
|
const TEXT_ACCENT: Color = Color::linear_rgb(0.20, 0.45, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ButtonColors {
|
impl Default for ButtonColors {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ButtonColors {
|
ButtonColors {
|
||||||
normal: Color::linear_rgb(0.15, 0.15, 0.15),
|
normal: NeumorphicColors::PRIMARY_NORMAL,
|
||||||
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
|
hovered: NeumorphicColors::PRIMARY_HOVERED,
|
||||||
|
pressed: NeumorphicColors::PRIMARY_PRESSED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,6 +59,18 @@ struct Menu;
|
|||||||
fn setup_menu(mut commands: Commands) {
|
fn setup_menu(mut commands: Commands) {
|
||||||
info!("menu");
|
info!("menu");
|
||||||
commands.spawn((Camera2d, Msaa::Off));
|
commands.spawn((Camera2d, Msaa::Off));
|
||||||
|
|
||||||
|
// Set neumorphic background
|
||||||
|
commands.spawn((
|
||||||
|
Node {
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NeumorphicColors::SURFACE),
|
||||||
|
));
|
||||||
|
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Node {
|
Node {
|
||||||
@@ -52,23 +89,27 @@ fn setup_menu(mut commands: Commands) {
|
|||||||
.spawn((
|
.spawn((
|
||||||
Button,
|
Button,
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(140.0),
|
width: Val::Px(180.0),
|
||||||
height: Val::Px(50.0),
|
height: Val::Px(65.0),
|
||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
|
border: UiRect::all(Val::Px(2.0)),
|
||||||
|
margin: UiRect::all(Val::Px(8.0)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
BackgroundColor(button_colors.normal),
|
BackgroundColor(button_colors.normal),
|
||||||
|
BorderColor(Color::linear_rgb(0.82, 0.84, 0.87)),
|
||||||
|
BorderRadius::all(Val::Px(16.0)),
|
||||||
button_colors,
|
button_colors,
|
||||||
ChangeState(GameState::Playing),
|
ChangeState(GameState::Playing),
|
||||||
))
|
))
|
||||||
.with_child((
|
.with_child((
|
||||||
Text::new("Play"),
|
Text::new("▶ PLAY"),
|
||||||
TextFont {
|
TextFont {
|
||||||
font_size: 40.0,
|
font_size: 28.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
TextColor(NeumorphicColors::TEXT_PRIMARY),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
commands
|
commands
|
||||||
@@ -85,74 +126,71 @@ fn setup_menu(mut commands: Commands) {
|
|||||||
Menu,
|
Menu,
|
||||||
))
|
))
|
||||||
.with_children(|children| {
|
.with_children(|children| {
|
||||||
|
let secondary_button_colors = ButtonColors {
|
||||||
|
normal: NeumorphicColors::SECONDARY_NORMAL,
|
||||||
|
hovered: NeumorphicColors::SECONDARY_HOVERED,
|
||||||
|
pressed: NeumorphicColors::SECONDARY_PRESSED,
|
||||||
|
};
|
||||||
|
|
||||||
children
|
children
|
||||||
.spawn((
|
.spawn((
|
||||||
Button,
|
Button,
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(170.0),
|
width: Val::Px(180.0),
|
||||||
height: Val::Px(50.0),
|
height: Val::Px(45.0),
|
||||||
justify_content: JustifyContent::SpaceAround,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
padding: UiRect::all(Val::Px(5.)),
|
padding: UiRect::all(Val::Px(8.)),
|
||||||
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
|
margin: UiRect::horizontal(Val::Px(8.0)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
BackgroundColor(Color::NONE),
|
BackgroundColor(secondary_button_colors.normal),
|
||||||
ButtonColors {
|
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||||
normal: Color::NONE,
|
BorderRadius::all(Val::Px(12.0)),
|
||||||
..default()
|
secondary_button_colors,
|
||||||
},
|
|
||||||
OpenLink("https://bevyengine.org"),
|
OpenLink("https://bevyengine.org"),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_child((
|
||||||
parent.spawn((
|
Text::new("🚀 Made with Bevy"),
|
||||||
Text::new("Made with Bevy"),
|
TextFont {
|
||||||
TextFont {
|
font_size: 14.0,
|
||||||
font_size: 15.0,
|
..default()
|
||||||
..default()
|
},
|
||||||
},
|
TextColor(NeumorphicColors::TEXT_SECONDARY),
|
||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
));
|
||||||
));
|
|
||||||
parent.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(32.),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
children
|
children
|
||||||
.spawn((
|
.spawn((
|
||||||
Button,
|
Button,
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(170.0),
|
width: Val::Px(180.0),
|
||||||
height: Val::Px(50.0),
|
height: Val::Px(45.0),
|
||||||
justify_content: JustifyContent::SpaceAround,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
padding: UiRect::all(Val::Px(5.)),
|
padding: UiRect::all(Val::Px(8.)),
|
||||||
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
|
margin: UiRect::horizontal(Val::Px(8.0)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
BackgroundColor(Color::NONE),
|
BackgroundColor(secondary_button_colors.normal),
|
||||||
|
BorderColor(Color::linear_rgb(0.80, 0.82, 0.85)),
|
||||||
|
BorderRadius::all(Val::Px(12.0)),
|
||||||
ButtonColors {
|
ButtonColors {
|
||||||
normal: Color::NONE,
|
normal: NeumorphicColors::SECONDARY_NORMAL,
|
||||||
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
|
hovered: NeumorphicColors::SECONDARY_HOVERED,
|
||||||
|
pressed: NeumorphicColors::SECONDARY_PRESSED,
|
||||||
},
|
},
|
||||||
OpenLink("https://github.com/NiklasEi/bevy_game_template"),
|
OpenLink("https://github.com/NiklasEi/bevy_game_template"),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_child((
|
||||||
parent.spawn((
|
Text::new("📖 Open Source"),
|
||||||
Text::new("Open source"),
|
TextFont {
|
||||||
TextFont {
|
font_size: 14.0,
|
||||||
font_size: 15.0,
|
..default()
|
||||||
..default()
|
},
|
||||||
},
|
TextColor(NeumorphicColors::TEXT_SECONDARY),
|
||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
));
|
||||||
));
|
|
||||||
parent.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(32.),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +216,10 @@ fn click_play_button(
|
|||||||
for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query {
|
for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query {
|
||||||
match *interaction {
|
match *interaction {
|
||||||
Interaction::Pressed => {
|
Interaction::Pressed => {
|
||||||
|
// Apply pressed state visual feedback
|
||||||
|
*color = button_colors.pressed.into();
|
||||||
|
|
||||||
|
// Handle button actions
|
||||||
if let Some(state) = change_state {
|
if let Some(state) = change_state {
|
||||||
next_state.set(state.0.clone());
|
next_state.set(state.0.clone());
|
||||||
} else if let Some(link) = open_link {
|
} else if let Some(link) = open_link {
|
||||||
@@ -187,9 +229,11 @@ fn click_play_button(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Interaction::Hovered => {
|
Interaction::Hovered => {
|
||||||
|
// Smooth transition to hovered state
|
||||||
*color = button_colors.hovered.into();
|
*color = button_colors.hovered.into();
|
||||||
}
|
}
|
||||||
Interaction::None => {
|
Interaction::None => {
|
||||||
|
// Return to normal state
|
||||||
*color = button_colors.normal.into();
|
*color = button_colors.normal.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user