mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-17 06:56:03 +01:00
feat(web/radial): radial menu text scaling (#639)
* radial menu improvements * addressing comments from Luke * removing uneeded comments * style(web/radial): remove icon scaling, increase number of items on page * style(web/radial): cursor pointer, remove stroke, container overflow --------- Co-authored-by: Luke <39926192+LukeWasTakenn@users.noreply.github.com>
This commit is contained in:
@@ -10,9 +10,10 @@ export const debugRadial = () => {
|
|||||||
{ icon: 'palette', label: 'Paint' },
|
{ icon: 'palette', label: 'Paint' },
|
||||||
{ iconWidth: 35, iconHeight: 35, icon: 'https://icon-library.com/images/white-icon-png/white-icon-png-18.jpg', label: 'External icon'},
|
{ iconWidth: 35, iconHeight: 35, icon: 'https://icon-library.com/images/white-icon-png/white-icon-png-18.jpg', label: 'External icon'},
|
||||||
{ icon: 'warehouse', label: 'Garage' },
|
{ icon: 'warehouse', label: 'Garage' },
|
||||||
{ icon: 'palette', label: 'Quite long \ntext' },
|
{ icon: 'palette', label: 'Quite Long Text' },
|
||||||
|
{ icon: 'palette', label: 'Fahrzeuginteraktionen' },
|
||||||
|
{ icon: 'palette', label: 'Fahrzeuginteraktionen' },
|
||||||
{ icon: 'palette', label: 'Paint' },
|
{ icon: 'palette', label: 'Paint' },
|
||||||
{ icon: 'warehouse', label: 'Garage' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ const useStyles = createStyles((theme) => ({
|
|||||||
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
fill: theme.fn.primaryColor(),
|
fill: theme.fn.primaryColor(),
|
||||||
|
cursor: 'pointer',
|
||||||
'> g > text, > g > svg > path': {
|
'> g > text, > g > svg > path': {
|
||||||
fill: '#fff',
|
fill: '#fff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'> g > text': {
|
'> g > text': {
|
||||||
fill: theme.colors.dark[0],
|
fill: theme.colors.dark[0],
|
||||||
|
strokeWidth: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgroundCircle: {
|
backgroundCircle: {
|
||||||
@@ -39,6 +41,7 @@ const useStyles = createStyles((theme) => ({
|
|||||||
stroke: theme.colors.dark[6],
|
stroke: theme.colors.dark[6],
|
||||||
strokeWidth: 4,
|
strokeWidth: 4,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
fill: theme.colors[theme.primaryColor][theme.fn.primaryShade() - 1],
|
fill: theme.colors[theme.primaryColor][theme.fn.primaryShade() - 1],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -54,14 +57,37 @@ const useStyles = createStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// includes More... button
|
const calculateFontSize = (text: string): number => {
|
||||||
const PAGE_ITEMS = 8;
|
if (text.length > 20) return 10;
|
||||||
|
if (text.length > 15) return 12;
|
||||||
|
return 13;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitTextIntoLines = (text: string, maxCharPerLine: number = 15): string[] => {
|
||||||
|
const words = text.split(' ');
|
||||||
|
const lines: string[] = [];
|
||||||
|
let currentLine = words[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < words.length; i++) {
|
||||||
|
if (currentLine.length + words[i].length + 1 <= maxCharPerLine) {
|
||||||
|
currentLine += ' ' + words[i];
|
||||||
|
} else {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = words[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(currentLine);
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PAGE_ITEMS = 6;
|
||||||
|
|
||||||
const degToRad = (deg: number) => deg * (Math.PI / 180);
|
const degToRad = (deg: number) => deg * (Math.PI / 180);
|
||||||
|
|
||||||
const RadialMenu: React.FC = () => {
|
const RadialMenu: React.FC = () => {
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const { locale } = useLocales();
|
const { locale } = useLocales();
|
||||||
|
const newDimension = 350 * 1.1025;
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [menuItems, setMenuItems] = useState<RadialMenuItem[]>([]);
|
const [menuItems, setMenuItems] = useState<RadialMenuItem[]>([]);
|
||||||
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean; page: number }>({
|
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean; page: number }>({
|
||||||
@@ -119,74 +145,83 @@ const RadialMenu: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScaleFade visible={visible}>
|
<ScaleFade visible={visible}>
|
||||||
<svg width="350px" height="350px" transform="rotate(90)">
|
<svg
|
||||||
{/*Fixed issues with background circle extending the circle when there's less than 3 items*/}
|
style={{ overflow: 'visible' }}
|
||||||
|
width={`${newDimension}px`}
|
||||||
|
height={`${newDimension}px`}
|
||||||
|
viewBox="0 0 350 350"
|
||||||
|
transform="rotate(90)"
|
||||||
|
>
|
||||||
|
{/* Fixed issues with background circle extending the circle when there's less than 3 items */}
|
||||||
<g transform="translate(175, 175)">
|
<g transform="translate(175, 175)">
|
||||||
<circle r={175} className={classes.backgroundCircle} />
|
<circle r={175} className={classes.backgroundCircle} />
|
||||||
</g>
|
</g>
|
||||||
{menuItems.map((item, index) => {
|
{menuItems.map((item, index) => {
|
||||||
// Always draw full circle to avoid elipse circles with 2 or less items
|
|
||||||
const pieAngle = 360 / (menuItems.length < 3 ? 3 : menuItems.length);
|
const pieAngle = 360 / (menuItems.length < 3 ? 3 : menuItems.length);
|
||||||
const angle = degToRad(pieAngle / 2 + 90);
|
const angle = degToRad(pieAngle / 2 + 90);
|
||||||
const gap = 0;
|
const gap = 1;
|
||||||
const radius = 175 * 0.65 - gap;
|
const radius = 175 * 0.65 - gap;
|
||||||
const sinAngle = Math.sin(angle);
|
const sinAngle = Math.sin(angle);
|
||||||
const cosAngle = Math.cos(angle);
|
const cosAngle = Math.cos(angle);
|
||||||
|
const iconYOffset = splitTextIntoLines(item.label, 15).length > 3 ? 3 : 0;
|
||||||
const iconX = 175 + sinAngle * radius;
|
const iconX = 175 + sinAngle * radius;
|
||||||
const iconY = 175 + cosAngle * radius;
|
const iconY = 175 + cosAngle * radius + iconYOffset; // Apply the Y offset to iconY
|
||||||
const iconWidth = Math.min(Math.max(item.iconWidth || 50, 0), 100);
|
const iconWidth = Math.min(Math.max(item.iconWidth || 50, 0), 100);
|
||||||
const iconHeight = Math.min(Math.max(item.iconHeight || 50, 0), 100);
|
const iconHeight = Math.min(Math.max(item.iconHeight || 50, 0), 100);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<g
|
||||||
<g
|
transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`}
|
||||||
transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`}
|
className={classes.sector}
|
||||||
className={classes.sector}
|
onClick={async () => {
|
||||||
onClick={async () => {
|
const clickIndex = menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) - (menu.page - 1) + index;
|
||||||
const clickIndex =
|
if (!item.isMore) fetchNui('radialClick', clickIndex);
|
||||||
menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) - (menu.page - 1) + index;
|
else {
|
||||||
if (!item.isMore) fetchNui('radialClick', clickIndex);
|
await changePage(true);
|
||||||
else {
|
}
|
||||||
await changePage(true);
|
}}
|
||||||
}
|
>
|
||||||
}}
|
<path
|
||||||
>
|
d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${
|
||||||
<path
|
175 + (175 - gap) * Math.cos(-degToRad(pieAngle))
|
||||||
d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${
|
}, ${175 + (175 - gap) * Math.sin(-degToRad(pieAngle))} z`}
|
||||||
175 + (175 - gap) * Math.cos(-degToRad(pieAngle))
|
/>
|
||||||
}, ${175 + (175 - gap) * Math.sin(-degToRad(pieAngle))} z`}
|
<g transform={`rotate(${index * pieAngle - 90} ${iconX} ${iconY})`} pointerEvents="none">
|
||||||
/>
|
{typeof item.icon === 'string' && isIconUrl(item.icon) ? (
|
||||||
<g transform={`rotate(${index * pieAngle - 90} ${iconX} ${iconY})`} pointerEvents="none">
|
<image
|
||||||
{typeof item.icon === 'string' && isIconUrl(item.icon) ? (
|
href={item.icon}
|
||||||
<image
|
width={iconWidth}
|
||||||
href={item.icon}
|
height={iconHeight}
|
||||||
width={iconWidth}
|
x={iconX - iconWidth / 2}
|
||||||
height={iconHeight}
|
y={iconY - iconHeight / 2 - iconHeight / 4}
|
||||||
x={iconX - iconWidth / 2}
|
/>
|
||||||
y={iconY - iconHeight / 2 - iconHeight / 4}
|
) : (
|
||||||
/>
|
<LibIcon
|
||||||
) : (
|
x={iconX - 14.5}
|
||||||
<LibIcon x={iconX - 12.5} y={iconY - 17.5} icon={item.icon as IconProp} width={25} height={25} fixedWidth/>
|
y={iconY - 17.5}
|
||||||
)}
|
icon={item.icon as IconProp}
|
||||||
<text
|
width={30}
|
||||||
x={iconX}
|
height={30}
|
||||||
y={iconY + (item.label.includes(' \n') ? 7 : 25)}
|
fixedWidth
|
||||||
fill="#fff"
|
/>
|
||||||
textAnchor="middle"
|
)}
|
||||||
pointerEvents="none"
|
<text
|
||||||
>
|
x={iconX}
|
||||||
{item.label.includes(' \n')
|
y={iconY + (splitTextIntoLines(item.label, 15).length > 2 ? 15 : 28)}
|
||||||
? item.label.split(' \n').map((value) => (
|
fill="#fff"
|
||||||
<tspan x={iconX} dy="1.2em">
|
textAnchor="middle"
|
||||||
{value}
|
fontSize={calculateFontSize(item.label)}
|
||||||
</tspan>
|
pointerEvents="none"
|
||||||
))
|
lengthAdjust="spacingAndGlyphs"
|
||||||
: item.label}
|
>
|
||||||
</text>
|
{splitTextIntoLines(item.label, 15).map((line, index) => (
|
||||||
</g>
|
<tspan x={iconX} dy={index === 0 ? 0 : '1.2em'} key={index}>
|
||||||
|
{line}
|
||||||
|
</tspan>
|
||||||
|
))}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
</>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<g
|
<g
|
||||||
@@ -202,7 +237,7 @@ const RadialMenu: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<circle r={32} className={classes.centerCircle} />
|
<circle r={28} className={classes.centerCircle} />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<div className={classes.centerIconContainer}>
|
<div className={classes.centerIconContainer}>
|
||||||
|
|||||||
Reference in New Issue
Block a user