diff options
Diffstat (limited to 'subprojects/frontend/src/DirectionalSplitPane.tsx')
-rw-r--r-- | subprojects/frontend/src/DirectionalSplitPane.tsx | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/subprojects/frontend/src/DirectionalSplitPane.tsx b/subprojects/frontend/src/DirectionalSplitPane.tsx new file mode 100644 index 00000000..59c8b739 --- /dev/null +++ b/subprojects/frontend/src/DirectionalSplitPane.tsx | |||
@@ -0,0 +1,159 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; | ||
8 | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||
9 | import Box from '@mui/material/Box'; | ||
10 | import Stack from '@mui/material/Stack'; | ||
11 | import { alpha, useTheme } from '@mui/material/styles'; | ||
12 | import { useCallback, useRef, useState } from 'react'; | ||
13 | import { useResizeDetector } from 'react-resize-detector'; | ||
14 | |||
15 | export default function DirectionalSplitPane({ | ||
16 | primary: left, | ||
17 | secondary: right, | ||
18 | primaryOnly: showLeftOnly, | ||
19 | secondaryOnly: showRightOnly, | ||
20 | }: { | ||
21 | primary: React.ReactNode; | ||
22 | secondary: React.ReactNode; | ||
23 | primaryOnly?: boolean; | ||
24 | secondaryOnly?: boolean; | ||
25 | }): JSX.Element { | ||
26 | const theme = useTheme(); | ||
27 | const stackRef = useRef<HTMLDivElement | null>(null); | ||
28 | const { ref: resizeRef, width, height } = useResizeDetector(); | ||
29 | const sliderRef = useRef<HTMLDivElement>(null); | ||
30 | const [resizing, setResizing] = useState(false); | ||
31 | const [fraction, setFraction] = useState(0.5); | ||
32 | |||
33 | const horizontalSplit = | ||
34 | width !== undefined && height !== undefined && height > width; | ||
35 | const direction = horizontalSplit ? 'column' : 'row'; | ||
36 | const axis = horizontalSplit ? 'height' : 'width'; | ||
37 | const primarySize = showLeftOnly | ||
38 | ? '100%' | ||
39 | : `calc(${fraction * 100}% - 0.5px)`; | ||
40 | const secondarySize = showRightOnly | ||
41 | ? '100%' | ||
42 | : `calc(${(1 - fraction) * 100}% - 0.5px)`; | ||
43 | const ref = useCallback( | ||
44 | (element: HTMLDivElement | null) => { | ||
45 | resizeRef(element); | ||
46 | stackRef.current = element; | ||
47 | }, | ||
48 | [resizeRef], | ||
49 | ); | ||
50 | |||
51 | return ( | ||
52 | <Stack | ||
53 | direction={direction} | ||
54 | height="100%" | ||
55 | width="100%" | ||
56 | overflow="hidden" | ||
57 | ref={ref} | ||
58 | > | ||
59 | {!showRightOnly && <Box {...{ [axis]: primarySize }}>{left}</Box>} | ||
60 | <Box | ||
61 | sx={{ | ||
62 | overflow: 'visible', | ||
63 | position: 'relative', | ||
64 | [axis]: '0px', | ||
65 | display: showLeftOnly || showRightOnly ? 'none' : 'flex', | ||
66 | flexDirection: direction, | ||
67 | [horizontalSplit | ||
68 | ? 'borderBottom' | ||
69 | : 'borderRight']: `1px solid ${theme.palette.outer.border}`, | ||
70 | }} | ||
71 | > | ||
72 | <Box | ||
73 | ref={sliderRef} | ||
74 | sx={{ | ||
75 | display: 'flex', | ||
76 | position: 'absolute', | ||
77 | [axis]: theme.spacing(2), | ||
78 | ...(horizontalSplit | ||
79 | ? { | ||
80 | top: theme.spacing(-1), | ||
81 | left: 0, | ||
82 | right: 0, | ||
83 | transform: 'translateY(0.5px)', | ||
84 | } | ||
85 | : { | ||
86 | left: theme.spacing(-1), | ||
87 | top: 0, | ||
88 | bottom: 0, | ||
89 | transform: 'translateX(0.5px)', | ||
90 | }), | ||
91 | zIndex: 999, | ||
92 | alignItems: 'center', | ||
93 | justifyContent: 'center', | ||
94 | color: theme.palette.text.secondary, | ||
95 | cursor: horizontalSplit ? 'ns-resize' : 'ew-resize', | ||
96 | '.MuiSvgIcon-root': { | ||
97 | opacity: resizing ? 1 : 0, | ||
98 | }, | ||
99 | ...(resizing | ||
100 | ? { | ||
101 | background: alpha( | ||
102 | theme.palette.text.primary, | ||
103 | theme.palette.action.activatedOpacity, | ||
104 | ), | ||
105 | } | ||
106 | : { | ||
107 | '&:hover': { | ||
108 | background: alpha( | ||
109 | theme.palette.text.primary, | ||
110 | theme.palette.action.hoverOpacity, | ||
111 | ), | ||
112 | '.MuiSvgIcon-root': { | ||
113 | opacity: 1, | ||
114 | }, | ||
115 | }, | ||
116 | }), | ||
117 | }} | ||
118 | onPointerDown={(event) => { | ||
119 | if (event.button !== 0) { | ||
120 | return; | ||
121 | } | ||
122 | sliderRef.current?.setPointerCapture(event.pointerId); | ||
123 | setResizing(true); | ||
124 | }} | ||
125 | onPointerUp={(event) => { | ||
126 | if (event.button !== 0) { | ||
127 | return; | ||
128 | } | ||
129 | sliderRef.current?.releasePointerCapture(event.pointerId); | ||
130 | setResizing(false); | ||
131 | }} | ||
132 | onPointerMove={(event) => { | ||
133 | if (!resizing) { | ||
134 | return; | ||
135 | } | ||
136 | const container = stackRef.current; | ||
137 | if (container === null) { | ||
138 | return; | ||
139 | } | ||
140 | const rect = container.getBoundingClientRect(); | ||
141 | const newFraction = horizontalSplit | ||
142 | ? (event.clientY - rect.top) / rect.height | ||
143 | : (event.clientX - rect.left) / rect.width; | ||
144 | setFraction(Math.min(0.9, Math.max(0.1, newFraction))); | ||
145 | }} | ||
146 | onDoubleClick={() => setFraction(0.5)} | ||
147 | > | ||
148 | {horizontalSplit ? <MoreHorizIcon /> : <MoreVertIcon />} | ||
149 | </Box> | ||
150 | </Box> | ||
151 | {!showLeftOnly && <Box {...{ [axis]: secondarySize }}>{right}</Box>} | ||
152 | </Stack> | ||
153 | ); | ||
154 | } | ||
155 | |||
156 | DirectionalSplitPane.defaultProps = { | ||
157 | primaryOnly: false, | ||
158 | secondaryOnly: false, | ||
159 | }; | ||