aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/DirectionalSplitPane.tsx
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-30 19:07:05 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-30 19:07:05 +0200
commite41e0391ba843b85a5b2890db95121fa39426a05 (patch)
tree4927c919cf3077ba9abb2cf31155de4d227b3119 /subprojects/frontend/src/DirectionalSplitPane.tsx
parentrefactor(frontend): filter dialog formatting (diff)
downloadrefinery-e41e0391ba843b85a5b2890db95121fa39426a05.tar.gz
refinery-e41e0391ba843b85a5b2890db95121fa39426a05.tar.zst
refinery-e41e0391ba843b85a5b2890db95121fa39426a05.zip
feat(frontend): window pane switcher
Diffstat (limited to 'subprojects/frontend/src/DirectionalSplitPane.tsx')
-rw-r--r--subprojects/frontend/src/DirectionalSplitPane.tsx159
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
7import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
8import MoreVertIcon from '@mui/icons-material/MoreVert';
9import Box from '@mui/material/Box';
10import Stack from '@mui/material/Stack';
11import { alpha, useTheme } from '@mui/material/styles';
12import { useCallback, useRef, useState } from 'react';
13import { useResizeDetector } from 'react-resize-detector';
14
15export 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
156DirectionalSplitPane.defaultProps = {
157 primaryOnly: false,
158 secondaryOnly: false,
159};