From 3ec88fc896d6b0676bd112eef8ed1eb3bc224f64 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 25 Mar 2023 18:12:09 -0700 Subject: [PATCH] Better grid layouts --- src-tauri/src/window_ext.rs | 2 +- src-web/components/AppRouter.tsx | 22 +- src-web/components/RequestPane.tsx | 13 +- src-web/components/RequestResponse.tsx | 112 ++++++++ src-web/components/ResponsePane.tsx | 10 +- src-web/components/Sidebar.tsx | 6 +- src-web/components/Workspace.tsx | 196 ++++++++++++++ .../components/layouts/WorkspaceLayout.tsx | 254 ------------------ 8 files changed, 329 insertions(+), 286 deletions(-) create mode 100644 src-web/components/RequestResponse.tsx create mode 100644 src-web/components/Workspace.tsx delete mode 100644 src-web/components/layouts/WorkspaceLayout.tsx diff --git a/src-tauri/src/window_ext.rs b/src-tauri/src/window_ext.rs index 91e05267..d3f072d3 100644 --- a/src-tauri/src/window_ext.rs +++ b/src-tauri/src/window_ext.rs @@ -1,7 +1,7 @@ use tauri::{Runtime, Window}; const TRAFFIC_LIGHT_OFFSET_X: f64 = 10.0; -const TRAFFIC_LIGHT_OFFSET_Y: f64 = 16.0; +const TRAFFIC_LIGHT_OFFSET_Y: f64 = 18.0; pub trait WindowExt { fn position_traffic_lights(&self); diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx index daed690f..ff28eab9 100644 --- a/src-web/components/AppRouter.tsx +++ b/src-web/components/AppRouter.tsx @@ -1,11 +1,9 @@ -import { lazy, Suspense } from 'react'; +import { Suspense } from 'react'; import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; import { routePaths } from '../hooks/useRoutes'; -import { WorkspaceLayout } from './layouts/WorkspaceLayout'; - -const Workspaces = lazy(() => import('./Workspaces')); -const Workspace = lazy(() => import('./Workspace')); -const RouteError = lazy(() => import('./RouteError')); +import Workspace from './Workspace'; +import RouteError from './RouteError'; +import Workspaces from './Workspaces'; const router = createBrowserRouter([ { @@ -22,22 +20,14 @@ const router = createBrowserRouter([ }, { path: routePaths.workspace({ workspaceId: ':workspaceId' }), - element: ( - - - - ), + element: , }, { path: routePaths.request({ workspaceId: ':workspaceId', requestId: ':requestId', }), - element: ( - - - - ), + element: , }, ], }, diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 8f89ef20..9d79c0db 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -78,25 +78,16 @@ export function RequestPane({ fullHeight, className }: Props) { return (
{activeRequest && ( <> - + diff --git a/src-web/components/RequestResponse.tsx b/src-web/components/RequestResponse.tsx new file mode 100644 index 00000000..e0c4ad9b --- /dev/null +++ b/src-web/components/RequestResponse.tsx @@ -0,0 +1,112 @@ +import classnames from 'classnames'; +import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { useKeyValue } from '../hooks/useKeyValue'; +import { RequestPane } from './RequestPane'; +import { ResponsePane } from './ResponsePane'; +import { ResizeBar } from './Workspace'; + +const rqst = { gridArea: 'rqst' }; +const resp = { gridArea: 'resp' }; +const drag = { gridArea: 'drag' }; + +export default function RequestResponse() { + const DEFAULT = 0.5; + const containerRef = useRef(null); + const widthKv = useKeyValue({ key: 'body_width', defaultValue: DEFAULT }); + const heightKv = useKeyValue({ key: 'body_height', defaultValue: DEFAULT }); + const width = widthKv.value ?? DEFAULT; + const height = heightKv.value ?? DEFAULT; + const [isResizing, setIsResizing] = useState(false); + const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( + null, + ); + + const vertical = false; + const styles = useMemo( + () => ({ + gridTemplate: vertical + ? ` + ' ${rqst.gridArea}' ${1 - height}fr + ' ${drag.gridArea}' auto + ' ${resp.gridArea}' ${height}fr + / 1fr + ` + : ` + ' ${rqst.gridArea} ${drag.gridArea} ${resp.gridArea}' minmax(0,1fr) + / ${1 - width}fr auto ${width}fr + `, + }), + [vertical, width, height], + ); + + const unsub = () => { + if (moveState.current !== null) { + document.documentElement.removeEventListener('mousemove', moveState.current.move); + document.documentElement.removeEventListener('mouseup', moveState.current.up); + } + }; + + const handleReset = useCallback( + () => (vertical ? heightKv.set(DEFAULT) : widthKv.set(DEFAULT)), + [vertical], + ); + + const handleResizeStart = useCallback( + (e: ReactMouseEvent) => { + if (containerRef.current === null) return; + unsub(); + + const containerRect = containerRef.current.getBoundingClientRect(); + + const mouseStartX = e.clientX; + const mouseStartY = e.clientY; + const startWidth = containerRect.width * width; + const startHeight = containerRect.height * height; + + moveState.current = { + move: (e: MouseEvent) => { + e.preventDefault(); // Prevent text selection and things + if (vertical) { + const newHeightPx = startHeight - (e.clientY - mouseStartY); + const newHeight = newHeightPx / containerRect.height; + heightKv.set(newHeight); + } else { + const newWidthPx = startWidth - (e.clientX - mouseStartX); + const newWidth = newWidthPx / containerRect.width; + widthKv.set(newWidth); + } + }, + up: (e: MouseEvent) => { + e.preventDefault(); + unsub(); + setIsResizing(false); + }, + }; + document.documentElement.addEventListener('mousemove', moveState.current.move); + document.documentElement.addEventListener('mouseup', moveState.current.up); + setIsResizing(true); + }, + [widthKv.value, heightKv.value, vertical], + ); + + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); +} diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 5e263c7e..18680952 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -16,7 +16,11 @@ import { HStack } from './core/Stacks'; import { StatusColor } from './core/StatusColor'; import { Webview } from './core/Webview'; -export const ResponsePane = memo(function ResponsePane() { +interface Props { + className?: string; +} + +export const ResponsePane = memo(function ResponsePane({ className }: Props) { const [pinnedResponseId, setPinnedResponseId] = useState(null); const activeRequestId = useActiveRequestId(); const responses = useResponses(activeRequestId); @@ -39,10 +43,10 @@ export const ResponsePane = memo(function ResponsePane() { ); return ( -
+
- + diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx new file mode 100644 index 00000000..76a50442 --- /dev/null +++ b/src-web/components/Workspace.tsx @@ -0,0 +1,196 @@ +import classnames from 'classnames'; +import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react'; +import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; +import { useWindowSize } from 'react-use'; +import { useSidebarDisplay } from '../hooks/useSidebarDisplay'; +import { Sidebar } from './Sidebar'; +import { WorkspaceHeader } from './WorkspaceHeader'; +import RequestResponse from './RequestResponse'; + +const side = { gridArea: 'side' }; +const head = { gridArea: 'head' }; +const body = { gridArea: 'body' }; + +export default function Workspace() { + const windowSize = useWindowSize(); + const styles = useMemo( + () => ({ + gridTemplate: ` + ' ${head.gridArea} ${head.gridArea}' auto + ' ${side.gridArea} ${body.gridArea}' minmax(0,1fr) + / auto 1fr + `, + }), + [], + ); + + return ( +
+ + + + + + + + + +
+ ); +} + +const BodyContainer = memo(function BodyContainer({ children }: { children: ReactNode }) { + return
{children}
; +}); + +const HeaderContainer = memo(function HeaderContainer({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +}); + +interface SidebarContainerProps { + children: ReactNode; + style: CSSProperties; + floating?: boolean; +} + +const SidebarContainer = memo(function SidebarContainer({ + children, + style, + floating, +}: SidebarContainerProps) { + const sidebar = useSidebarDisplay(); + const [isResizing, setIsResizing] = useState(false); + const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( + null, + ); + + const unsub = () => { + if (moveState.current !== null) { + document.documentElement.removeEventListener('mousemove', moveState.current.move); + document.documentElement.removeEventListener('mouseup', moveState.current.up); + } + }; + + const handleResizeStart = useCallback( + (e: ReactMouseEvent) => { + if (sidebar.width === undefined) return; + + unsub(); + const mouseStartX = e.clientX; + const startWidth = sidebar.width; + moveState.current = { + move: (e: MouseEvent) => { + e.preventDefault(); // Prevent text selection and things + sidebar.set(startWidth + (e.clientX - mouseStartX)); + }, + up: (e: MouseEvent) => { + e.preventDefault(); + unsub(); + setIsResizing(false); + }, + }; + document.documentElement.addEventListener('mousemove', moveState.current.move); + document.documentElement.addEventListener('mouseup', moveState.current.up); + setIsResizing(true); + }, + [sidebar.width], + ); + + const sidebarStyles = useMemo( + () => ({ + width: sidebar.hidden ? 0 : sidebar.width, // No width when hidden + borderWidth: sidebar.hidden ? 0 : undefined, // No border when hidden + }), + [sidebar.width, sidebar.hidden, style], + ); + + const commonClassname = classnames('overflow-hidden bg-gray-100 border-highlight'); + + if (floating) { + return ( +
+
+ {children} +
+
+ ); + } + + return ( +
+ + {children} +
+ ); +}); + +interface ResizeBarProps { + className?: string; + barClassName?: string; + isResizing: boolean; + onResizeStart: (e: ReactMouseEvent) => void; + onReset?: () => void; + side: 'left' | 'right' | 'top'; + justify: 'center' | 'end' | 'start'; +} + +export function ResizeBar({ + justify, + className, + onResizeStart, + onReset, + isResizing, + side, +}: ResizeBarProps) { + const vertical = side === 'top'; + return ( +
+ {/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */} + {isResizing && ( +
+ )} +
+ ); +} diff --git a/src-web/components/layouts/WorkspaceLayout.tsx b/src-web/components/layouts/WorkspaceLayout.tsx deleted file mode 100644 index c4684f41..00000000 --- a/src-web/components/layouts/WorkspaceLayout.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import classnames from 'classnames'; -import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react'; -import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; -import { useWindowSize } from 'react-use'; -import { useKeyValue } from '../../hooks/useKeyValue'; -import { useSidebarDisplay } from '../../hooks/useSidebarDisplay'; -import { RequestPane } from '../RequestPane'; -import { ResponsePane } from '../ResponsePane'; -import { Sidebar } from '../Sidebar'; -import { WorkspaceHeader } from '../WorkspaceHeader'; - -const side = { gridArea: 'side' }; -const head = { gridArea: 'head' }; -const rqst = { gridArea: 'rqst' }; -const resp = { gridArea: 'resp' }; - -export function WorkspaceLayout() { - const windowSize = useWindowSize(); - const vertical = windowSize.width < 800; - const styles = useMemo( - () => - vertical - ? { - gridTemplate: ` - ' ${head.gridArea} ${head.gridArea}' auto - ' ${side.gridArea} ${rqst.gridArea}' 1fr - ' ${side.gridArea} ${resp.gridArea}' 1fr - / auto 1fr - `, - } - : { - gridTemplate: ` - ' ${head.gridArea} ${head.gridArea} ${head.gridArea}' auto - ' ${side.gridArea} ${rqst.gridArea} ${resp.gridArea}' 1fr - / auto 1fr auto - `, - }, - [vertical], - ); - - return ( -
- - - - - - - - - - - - -
- ); -} - -const HeaderContainer = memo(function HeaderContainer({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -}); - -const RequestContainer = memo(function RequestContainer({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -}); - -const ResponseContainer = memo(function ResponseContainer({ children }: { children: ReactNode }) { - const displayKv = useKeyValue({ key: 'response_width', defaultValue: 400 }); - const [isResizing, setIsResizing] = useState(false); - const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( - null, - ); - const unsub = () => { - if (moveState.current !== null) { - document.documentElement.removeEventListener('mousemove', moveState.current.move); - document.documentElement.removeEventListener('mouseup', moveState.current.up); - } - }; - - const handleReset = useCallback(() => displayKv.set(500), []); - - const handleResizeStart = useCallback( - (e: ReactMouseEvent) => { - if (displayKv.value === undefined) return; - - unsub(); - const mouseStartX = e.clientX; - const startWidth = displayKv.value; - moveState.current = { - move: (e: MouseEvent) => { - e.preventDefault(); // Prevent text selection and things - displayKv.set(startWidth - (e.clientX - mouseStartX)); - }, - up: (e: MouseEvent) => { - e.preventDefault(); - unsub(); - setIsResizing(false); - }, - }; - document.documentElement.addEventListener('mousemove', moveState.current.move); - document.documentElement.addEventListener('mouseup', moveState.current.up); - setIsResizing(true); - }, - [displayKv.value], - ); - - const sidebarStyles = useMemo( - () => ({ - width: displayKv.value, // No width when hidden - }), - [displayKv.value], - ); - - return ( -
- - {children} -
- ); -}); - -interface SidebarContainerProps { - children: ReactNode; - style: CSSProperties; - floating?: boolean; -} - -const SidebarContainer = memo(function SidebarContainer({ - children, - style, - floating, -}: SidebarContainerProps) { - const sidebar = useSidebarDisplay(); - const [isResizing, setIsResizing] = useState(false); - const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( - null, - ); - const unsub = () => { - if (moveState.current !== null) { - document.documentElement.removeEventListener('mousemove', moveState.current.move); - document.documentElement.removeEventListener('mouseup', moveState.current.up); - } - }; - - const handleResizeStart = useCallback( - (e: ReactMouseEvent) => { - if (sidebar.width === undefined) return; - - unsub(); - const mouseStartX = e.clientX; - const startWidth = sidebar.width; - moveState.current = { - move: (e: MouseEvent) => { - e.preventDefault(); // Prevent text selection and things - sidebar.set(startWidth + (e.clientX - mouseStartX)); - }, - up: (e: MouseEvent) => { - e.preventDefault(); - unsub(); - setIsResizing(false); - }, - }; - document.documentElement.addEventListener('mousemove', moveState.current.move); - document.documentElement.addEventListener('mouseup', moveState.current.up); - setIsResizing(true); - }, - [sidebar.width], - ); - - const sidebarStyles = useMemo( - () => ({ - ...style, - width: sidebar.hidden ? 0 : sidebar.width, // No width when hidden - borderWidth: sidebar.hidden ? 0 : undefined, // No border when hidden - }), - [sidebar.width, sidebar.hidden, style], - ); - - const commonClassname = classnames('overflow-hidden bg-gray-100 border-highlight'); - - if (floating) { - return ( -
- {children} -
- ); - } - - return ( -
- - {children} -
- ); -}); - -interface ResizeBarProps { - isResizing: boolean; - onResizeStart: (e: ReactMouseEvent) => void; - onReset?: () => void; - side: 'left' | 'right'; -} - -function ResizeBar({ onResizeStart, onReset, isResizing, side }: ResizeBarProps) { - return ( -
- {/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */} - {isResizing &&
} -
-
- ); -}