Skip to content

Commit

Permalink
chore(web): add beta editor layout (#463)
Browse files Browse the repository at this point in the history
  • Loading branch information
isoppp authored Jun 5, 2023
1 parent 5ed8195 commit 96c4730
Show file tree
Hide file tree
Showing 21 changed files with 645 additions and 88 deletions.
118 changes: 118 additions & 0 deletions web/src/beta/components/Resizable/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, { useCallback, useState, useEffect, useMemo } from "react";

type Direction = "vertical" | "horizontal";
type Gutter = "start" | "end";

const getPositionFromEvent = (e: React.MouseEvent | React.TouchEvent) => {
const { nativeEvent } = e;
if (nativeEvent instanceof MouseEvent) {
const { clientX: x, clientY: y, which } = nativeEvent;

// When user click with right button the resize is stuck in resizing mode until users clicks again, dont continue if right click is used.
// https://github.com/bokuweb/re-resizable/blob/06dd269f835a201b03b4f62f37533784d855fdd2/src/index.tsx#L611
if (which === 3) return;

return { x, y };
}

if (nativeEvent instanceof TouchEvent) {
const touch = nativeEvent.touches[0];
const { clientX: x, clientY: y } = touch;

return { x, y };
}

return;
};

const getDelta = (direction: Direction, deltaX: number, deltaY: number) =>
direction === "vertical" ? deltaX : deltaY;

const getSize = (size: number, delta: number, minSize?: number, maxSize?: number) => {
if (minSize !== undefined && size + delta < minSize) return minSize;
if (maxSize !== undefined && size + delta > maxSize) return maxSize;
return size + delta;
};

export default (
direction: Direction,
gutter: Gutter,
initialSize: number,
minSize?: number,
maxSize?: number,
) => {
const [isResizing, setIsResizing] = useState(false);
const [size, setSize] = useState(initialSize);
const [position, setPosition] = useState({ x: 0, y: 0 });

const onResizeStart = useCallback(
(e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
const position = getPositionFromEvent(e);
if (!position) return;

setIsResizing(true);
setPosition(position);
},
[],
);

const onResize = useCallback(
(e: MouseEvent | TouchEvent) => {
if (!isResizing) return;

const { clientX: x, clientY: y } = e instanceof MouseEvent ? e : e.touches[0];
const deltaX = gutter === "start" ? position.x - x : x - position.x;
const deltaY = gutter === "start" ? position.y - y : y - position.y;
const delta = getDelta(direction, deltaX, deltaY);

setSize(getSize(size, delta, minSize, maxSize));
setPosition({ x, y });
},
[isResizing, position, direction, gutter, size, minSize, maxSize],
);

let unbind = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function

const onResizeEnd = useCallback(() => {
if (!isResizing) return;

setIsResizing(false);
setPosition({ x: 0, y: 0 });
unbind();
}, [isResizing]);

const bindEventListeners = useCallback(() => {
if (typeof window === "undefined") return;
window.addEventListener("mouseup", onResizeEnd);
window.addEventListener("mousemove", onResize);
window.addEventListener("mouseleave", onResizeEnd);
window.addEventListener("touchmove", onResize);
window.addEventListener("touchend", onResizeEnd);
}, [onResize, onResizeEnd]);

const unbindEventListeners = useCallback(() => {
if (typeof window === "undefined") return;
window.removeEventListener("mouseup", onResizeEnd);
window.removeEventListener("mousemove", onResize);
window.removeEventListener("mouseleave", onResizeEnd);
window.removeEventListener("touchmove", onResize);
window.removeEventListener("touchend", onResizeEnd);
}, [onResize, onResizeEnd]);

unbind = unbindEventListeners;

useEffect(() => {
bindEventListeners();
return () => unbindEventListeners();
});

const gutterProps = useMemo(
() => ({
onMouseDown: (e: React.MouseEvent<HTMLDivElement>) => onResizeStart(e),
onTouchStart: (e: React.TouchEvent<HTMLDivElement>) => onResizeStart(e),
}),
[onResizeStart],
);

return { size, gutterProps };
};
55 changes: 55 additions & 0 deletions web/src/beta/components/Resizable/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Meta, StoryObj } from "@storybook/react";
import { ReactNode, CSSProperties, ComponentProps } from "react";

import Resizable from ".";

const Container: React.FC<{ children?: ReactNode; style?: CSSProperties }> = ({
children,
style,
}) => <div style={{ display: "flex", height: 400, ...style }}>{children}</div>;
const Pane = <div style={{ flex: 1, background: "#ffffff" }} />;
const Content = (
<div style={{ width: "100%", height: "100%", background: "#ffffff", color: "#000000" }}>
content
</div>
);

export default {
component: Resizable,
} as Meta<ComponentProps<typeof Resizable>>;

export const Vertical: StoryObj<typeof Resizable> = {
args: {
direction: "vertical",
gutter: "end",
size: 400,
minSize: 300,
maxSize: 500,
},
render: args => {
return (
<Container style={{ flexDirection: "row" }}>
<Resizable {...args}>{Content}</Resizable>
{Pane}
</Container>
);
},
};

export const Horizontal: StoryObj<typeof Resizable> = {
args: {
direction: "horizontal",
gutter: "end",
size: 200,
minSize: 100,
maxSize: 300,
},
render: args => {
return (
<Container style={{ flexDirection: "column" }}>
<Resizable {...args}>{Content}</Resizable>
{Pane}
</Container>
);
},
};
78 changes: 78 additions & 0 deletions web/src/beta/components/Resizable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ReactNode } from "react";

import { styled } from "@reearth/services/theme";

import useHooks from "./hooks";

type Props = {
children?: ReactNode;
direction: "vertical" | "horizontal";
gutter: "start" | "end";
size: number;
minSize?: number;
maxSize?: number;
};

const Resizable: React.FC<Props> = ({
direction,
gutter,
size: initialSize,
minSize,
maxSize,
children,
}) => {
const { size, gutterProps } = useHooks(direction, gutter, initialSize, minSize, maxSize);

const showTopGutter = direction === "horizontal" && gutter === "start";
const showRightGutter = direction === "vertical" && gutter === "end";
const showBottomGutter = direction === "horizontal" && gutter === "end";
const showLeftGutter = direction === "vertical" && gutter === "start";

const TopGutter = showTopGutter ? <HorizontalGutter {...gutterProps} /> : null;
const RightGutter = showRightGutter ? <VerticalGutter {...gutterProps} /> : null;
const BottomGutter = showBottomGutter ? <HorizontalGutter {...gutterProps} /> : null;
const LeftGutter = showLeftGutter ? <VerticalGutter {...gutterProps} /> : null;

return (
<StyledResizable direction={direction} size={size}>
{TopGutter}
{LeftGutter}
<Wrapper>{children}</Wrapper>
{RightGutter}
{BottomGutter}
</StyledResizable>
);
};

const StyledResizable = styled.div<Pick<Props, "direction" | "size">>`
display: flex;
align-items: stretch;
flex-direction: ${({ direction }) => (direction === "vertical" ? "row" : "column")};
width: ${({ direction, size }) => (direction === "horizontal" ? null : `${size}px`)};
height: ${({ direction, size }) => (direction === "vertical" ? null : `${size}px`)};
flex-shrink: 0;
`;

const Wrapper = styled.div`
width: calc(100% - 4px);
height: 100%;
background: ${props => props.theme.main.deepestBg};
`;

const Gutter = styled.div`
background: #000000;
user-select: none;
background: ${props => props.theme.main.deepestBg};
`;

const HorizontalGutter = styled(Gutter)`
height: 4px;
cursor: row-resize;
`;

const VerticalGutter = styled(Gutter)`
width: 4px;
cursor: col-resize;
`;

export default Resizable;
86 changes: 86 additions & 0 deletions web/src/beta/features/Editor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Resizable from "@reearth/beta/components/Resizable";
import useLeftPanel from "@reearth/beta/features/Editor/useLeftPanel";
import useRightPanel from "@reearth/beta/features/Editor/useRightPanel";
import useVisualizerNav from "@reearth/beta/features/Editor/useVisualizerNav";
import Navbar, { Tab } from "@reearth/beta/features/Navbar";
import Visualizer from "@reearth/beta/features/Visualizer";
import { Provider as DndProvider } from "@reearth/beta/utils/use-dnd";
import { metrics, styled } from "@reearth/services/theme";

type Props = {
sceneId: string;
tab: Tab;
};

const Editor: React.FC<Props> = ({ sceneId, tab }) => {
const { leftPanel } = useLeftPanel({ tab });
const { rightPanel } = useRightPanel({ tab });
const { visualizerNav } = useVisualizerNav({ tab });

return (
<DndProvider>
<Wrapper>
<Navbar sceneId={sceneId} currentTab={tab} />
<MainSection>
{leftPanel && (
<Resizable
direction="vertical"
gutter="end"
size={metrics.propertyMenuMinWidth}
minSize={metrics.propertyMenuMinWidth}
maxSize={metrics.propertyMenuMaxWidth}>
{leftPanel}
</Resizable>
)}
<Center>
<VisualizerWrapper hasNav={!!visualizerNav}>
{visualizerNav && <div>{visualizerNav}</div>}
<Visualizer />
</VisualizerWrapper>
</Center>
{rightPanel && (
<Resizable
direction="vertical"
gutter="start"
size={metrics.propertyMenuMinWidth}
minSize={metrics.propertyMenuMinWidth}
maxSize={metrics.propertyMenuMaxWidth}>
{rightPanel}
</Resizable>
)}
</MainSection>
</Wrapper>
</DndProvider>
);
};

export default Editor;

const Wrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
color: ${({ theme }) => theme.main.text};
`;

const MainSection = styled.div`
display: flex;
flex-grow: 1;
height: 100%;
background-color: ${({ theme }) => theme.main.deepestBg};
`;

const Center = styled.div`
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
`;

const VisualizerWrapper = styled.div<{ hasNav: boolean }>`
${({ hasNav, theme }) => hasNav && `border: 1px solid ${theme.main.deepBg}`};
height: 100%;
border-radius: 4px;
flex-grow: 1;
`;
Loading

0 comments on commit 96c4730

Please sign in to comment.