Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code editor paneling #1334

Merged
merged 19 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions quadratic-client/src/app/gridGL/QuadraticGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { events } from '@/app/events/events';
import { ImportProgress } from '@/app/ui/components/ImportProgress';
import { Search } from '@/app/ui/components/Search';
import { MouseEvent, useCallback, useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { editorInteractionStateAtom } from '../atoms/editorInteractionStateAtom';
Expand All @@ -16,13 +17,12 @@ let spaceIsDown = false;
export default function QuadraticGrid() {
const [container, setContainer] = useState<HTMLDivElement>();
const containerRef = useCallback((node: HTMLDivElement | null) => {
if (node) setContainer(node);
if (node) {
setContainer(node);
pixiApp.attach(node);
}
}, []);

useEffect(() => {
if (container) pixiApp.attach(container);
}, [container]);

const [panMode, setPanMode] = useState<PanMode>(PanMode.Disabled);
useEffect(() => {
const updatePanMode = (panMode: PanMode) => {
Expand Down Expand Up @@ -119,6 +119,7 @@ export default function QuadraticGrid() {
<HTMLGridContainer parent={container} />
<FloatingContextMenu container={container} showContextMenu={showContextMenu} />
<ImportProgress />
<Search />
</div>
);
}
4 changes: 4 additions & 0 deletions quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ export class PixiApp {
this.canvas.className = 'pixi_canvas';
this.canvas.tabIndex = 0;

const observer = new ResizeObserver(this.resize);
observer.observe(this.canvas);

const resolution = Math.max(2, window.devicePixelRatio);
this.renderer = new Renderer({
view: this.canvas,
Expand Down Expand Up @@ -209,6 +212,7 @@ export class PixiApp {
this.axesLines.dirty = true;
this.headings.dirty = true;
this.cursor.dirty = true;
this.render();
};

// called before and after a render
Expand Down
2 changes: 1 addition & 1 deletion quadratic-client/src/app/ui/QuadraticUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function QuadraticUI() {
// Resize the canvas when user goes in/out of presentation mode
useEffect(() => {
pixiApp.resize();
}, [presentationMode]);
}, [presentationMode, editorInteractionState.showCodeEditor]);

return (
<div
Expand Down
9 changes: 8 additions & 1 deletion quadratic-client/src/app/ui/components/FileUploadWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@ export const FileUploadWrapper = (props: PropsWithChildren) => {
<div
ref={divRef}
onDragEnter={handleDrag}
style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', position: 'relative' }}
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
position: 'relative',
minWidth: 0,
}}
>
{props.children}
{dragActive && (
Expand Down
12 changes: 4 additions & 8 deletions quadratic-client/src/app/ui/components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,15 @@ export function Search() {
}
}, [editorInteractionState.showSearch]);

useEffect(() => {
if (editorInteractionState.showSearch && editorInteractionState.showCodeEditor) {
setEditorInteractionState((prev) => ({ ...prev, showSearch: false }));
}
}, [editorInteractionState.showSearch, editorInteractionState.showCodeEditor, setEditorInteractionState]);

return (
<Popover open={!!editorInteractionState.showSearch}>
<PopoverAnchor className="absolute right-[.5rem] top-[100%] min-[400px]:top-[calc(100%+.5rem)]" />
<PopoverAnchor />
<PopoverContent
align="end"
className="flex w-[100vw] flex-col items-center gap-1 p-2 min-[400px]:w-[400px] min-[400px]:flex-row min-[400px]:p-3"
className="m-2 flex w-[100vw] flex-col items-center gap-1 p-2 min-[400px]:w-[400px] min-[400px]:flex-row min-[400px]:p-3"
onKeyDown={(e) => {
e.stopPropagation();

// close search
if (e.key === 'Escape') {
setEditorInteractionState((prev) => ({ ...prev, showSearch: false }));
Expand Down
202 changes: 26 additions & 176 deletions quadratic-client/src/app/ui/menus/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useRecoilState } from 'recoil';
// TODO(ddimaria): leave this as we're looking to add this back in once improved
// import { Diagnostic } from 'vscode-languageserver-types';
import useLocalStorage from '@/shared/hooks/useLocalStorage';
import { CodeEditorPanels } from '@/app/ui/menus/CodeEditor/CodeEditorPanels';
import { useCodeEditorPanelData } from '@/app/ui/menus/CodeEditor/useCodeEditorPanelData';
import { cn } from '@/shared/shadcn/utils';
import { googleAnalyticsAvailable } from '@/shared/utils/analytics';
import { hasPermissionToEditFile } from '../../../actions';
Expand All @@ -24,16 +25,9 @@ import { CodeEditorBody } from './CodeEditorBody';
import { CodeEditorProvider } from './CodeEditorContext';
import { CodeEditorHeader } from './CodeEditorHeader';
import { Console } from './Console';
import { ResizeControl } from './ResizeControl';
import { ReturnTypeInspector } from './ReturnTypeInspector';
import { SaveChangesAlert } from './SaveChangesAlert';

const MIN_WIDTH_EDITOR = 350;
const MIN_WIDTH_PANEL = 300;
const MIN_WIDTH_VISIBLE_GRID = 150;

export type PanelPosition = 'bottom' | 'left';

export const dispatchEditorAction = (name: string) => {
window.dispatchEvent(new CustomEvent('run-editor-action', { detail: name }));
};
Expand All @@ -43,17 +37,6 @@ export const CodeEditor = () => {
const { showCodeEditor, mode: editorMode } = editorInteractionState;

const { pythonState } = usePythonState();
const [editorWidth, setEditorWidth] = useLocalStorage<number>(
'codeEditorWidth',
window.innerWidth * 0.35 // default to 35% of the window width
);
const [editorHeightPercentage, setEditorHeightPercentage] = useLocalStorage<number>('codeEditorHeightPercentage', 75);
const [panelWidth, setPanelWidth] = useLocalStorage('codeEditorPanelWidth', MIN_WIDTH_PANEL);
const [panelHeightPercentage, setPanelHeightPercentage] = useLocalStorage<number>(
'codeEditorPanelHeightPercentage',
50
);
const [panelPosition, setPanelPosition] = useLocalStorage<PanelPosition>('codeEditorPanelPosition', 'bottom');
const containerRef = useRef<HTMLDivElement>(null);

// update code cell
Expand Down Expand Up @@ -323,53 +306,7 @@ export const CodeEditor = () => {
}
};

// Whenever we change the position of the panel to be left-to-right, make sure
// there's enough width for the editor and the panel
useEffect(() => {
if (panelPosition === 'left') {
if (editorWidth + panelWidth > window.innerWidth - MIN_WIDTH_VISIBLE_GRID) {
setPanelWidth(MIN_WIDTH_PANEL);
setEditorWidth(window.innerWidth - MIN_WIDTH_PANEL - MIN_WIDTH_VISIBLE_GRID);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [panelPosition]);

// When the window resizes, recalculate the appropriate proportions for
// the editor and the panel
useEffect(() => {
const handleResize = (event: any) => {
const width = event.target.innerWidth;

if (width < 1024) return;

const availableWidth = width - MIN_WIDTH_VISIBLE_GRID;
if (panelPosition === 'left' && panelWidth + editorWidth > availableWidth) {
const totalOldWidth = editorWidth + panelWidth;

setEditorWidth((oldEditorWidth) => {
const editorPercentage = oldEditorWidth / totalOldWidth;
return availableWidth * editorPercentage;
});

setPanelWidth((oldPanelWidth) => {
const panelPercentage = oldPanelWidth / totalOldWidth;
return availableWidth * panelPercentage;
});
} else if (panelPosition === 'bottom' && editorWidth > availableWidth) {
const totalOldWidth = editorWidth;
setEditorWidth((oldEditorWidth) => {
const editorPercentage = oldEditorWidth / totalOldWidth;
return availableWidth * editorPercentage;
});
}
};

window.addEventListener('resize', handleResize, true);
return () => {
window.removeEventListener('resize', handleResize, true);
};
}, [editorWidth, panelPosition, panelWidth, setEditorWidth, setPanelWidth]);
const codeEditorPanelData = useCodeEditorPanelData();

if (!showCodeEditor) {
return null;
Expand All @@ -379,27 +316,28 @@ export const CodeEditor = () => {
<CodeEditorProvider>
<div
ref={containerRef}
className={cn(
'absolute bottom-0 right-0 top-0 z-[2] flex bg-background',
panelPosition === 'left' ? '' : 'flex-col'
)}
style={{ width: `${editorWidth + (panelPosition === 'left' ? panelWidth : 0)}px` }}
className={cn('relative flex bg-background', codeEditorPanelData.panelPosition === 'left' ? '' : 'flex-col')}
style={{
width: `${
codeEditorPanelData.editorWidth +
(codeEditorPanelData.panelPosition === 'left' ? codeEditorPanelData.panelWidth : 0)
}px`,
borderLeft: '1px solid black',
}}
>
<div
id="QuadraticCodeEditorID"
className={cn('flex flex-col', panelPosition === 'left' ? 'order-2' : 'order-1')}
className={cn('flex flex-col', codeEditorPanelData.panelPosition === 'left' ? 'order-2' : 'order-1')}
style={{
width: `${editorWidth}px`,
height: panelPosition === 'left' ? '100%' : `${editorHeightPercentage}%`,
width: `${codeEditorPanelData.editorWidth}px`,
height:
codeEditorPanelData.panelPosition === 'left' ? '100%' : `${codeEditorPanelData.editorHeightPercentage}%`,
}}
onKeyDownCapture={onKeyDownEditor}
onPointerEnter={() => {
// todo: handle multiplayer code editor here
multiplayer.sendMouseMove();
}}
onPointerMove={(e) => {
e.stopPropagation();
}}
>
{showSaveChangesAlert && (
<SaveChangesAlert
Expand Down Expand Up @@ -441,15 +379,19 @@ export const CodeEditor = () => {
show={Boolean(evaluationResult?.line_number && !out?.stdErr && !unsaved)}
/>
)}

{/* Console Wrapper */}
</div>

<div
className={cn(panelPosition === 'left' ? 'order-1' : 'order-2', 'relative flex flex-col bg-background')}
className={cn(
codeEditorPanelData.panelPosition === 'left' ? 'order-1' : 'order-2',
'relative flex flex-col bg-background'
)}
style={{
width: panelPosition === 'left' ? `${panelWidth}px` : '100%',
height: panelPosition === 'left' ? '100%' : `${100 - editorHeightPercentage}%`,
width: codeEditorPanelData.panelPosition === 'left' ? `${codeEditorPanelData.panelWidth}px` : '100%',
height:
codeEditorPanelData.panelPosition === 'left'
? '100%'
: `${100 - codeEditorPanelData.editorHeightPercentage}%`,
}}
>
<Console
Expand All @@ -458,102 +400,10 @@ export const CodeEditor = () => {
editorContent={editorContent}
evaluationResult={evaluationResult}
spillError={spillError}
panelPosition={panelPosition}
setPanelPosition={setPanelPosition}
panelHeightPercentage={panelHeightPercentage}
codeEditorPanelData={codeEditorPanelData}
/>
</div>

{panelPosition === 'left' && (
<>
{/* left-to-right: height of sections in panel */}
<ResizeControl
style={{ top: panelHeightPercentage + '%', width: panelWidth + 'px' }}
setState={(mouseEvent) => {
if (!containerRef.current) return;

const containerRect = containerRef.current?.getBoundingClientRect();
const newValue = ((mouseEvent.clientY - containerRect.top) / containerRect.height) * 100;
if (newValue >= 25 && newValue <= 75) {
setPanelHeightPercentage(newValue);
}
}}
position="HORIZONTAL"
/>
{/* left-to-right: outer edge */}
<ResizeControl
style={{ left: `-1px` }}
setState={(mouseEvent) => {
const offsetFromRight = window.innerWidth - mouseEvent.x;
const min = MIN_WIDTH_PANEL + MIN_WIDTH_EDITOR;
const max = window.innerWidth - MIN_WIDTH_VISIBLE_GRID;

if (offsetFromRight > min && offsetFromRight < max) {
const totalOldWidth = editorWidth + panelWidth;
setEditorWidth((oldEditorWidth) => {
const editorPercentage = oldEditorWidth / totalOldWidth;
const newValue = offsetFromRight * editorPercentage;
return newValue > MIN_WIDTH_EDITOR ? newValue : MIN_WIDTH_EDITOR;
});
setPanelWidth((oldPanelWidth) => {
const panelPercentage = oldPanelWidth / totalOldWidth;
const newValue = offsetFromRight * panelPercentage;
return newValue > MIN_WIDTH_PANEL ? newValue : MIN_WIDTH_PANEL;
});
}
}}
position="VERTICAL"
/>
{/* left-to-right: middle line */}
<ResizeControl
style={{ left: `${panelWidth}px` }}
setState={(mouseEvent) => {
const offsetFromRight = window.innerWidth - mouseEvent.x;
const totalWidth = editorWidth + panelWidth;
const newEditorWidth = offsetFromRight;
const newPanelWidth = totalWidth - offsetFromRight;

if (newEditorWidth > MIN_WIDTH_EDITOR && newPanelWidth > MIN_WIDTH_PANEL) {
setEditorWidth(newEditorWidth);
setPanelWidth(newPanelWidth);
}
}}
position="VERTICAL"
/>
</>
)}

{panelPosition === 'bottom' && (
<>
{/* top-to-bottom: editor width */}
<ResizeControl
style={{ left: '-1px' }}
setState={(mouseEvent) => {
const offsetFromRight = window.innerWidth - mouseEvent.x;
const min = MIN_WIDTH_EDITOR;
const max = window.innerWidth - MIN_WIDTH_VISIBLE_GRID;
const newValue = offsetFromRight > max ? max : offsetFromRight < min ? min : offsetFromRight;
setEditorWidth(newValue);
}}
position="VERTICAL"
/>
{/* top-to-bottom: height of sections */}
<ResizeControl
style={{ top: editorHeightPercentage + '%', width: '100%' }}
setState={(mouseEvent) => {
if (!containerRef.current) return;

const containerRect = containerRef.current?.getBoundingClientRect();
const newTopHeight = ((mouseEvent.clientY - containerRect.top) / containerRect.height) * 100;

if (newTopHeight >= 25 && newTopHeight <= 75) {
setEditorHeightPercentage(newTopHeight);
}
}}
position="HORIZONTAL"
/>
</>
)}
<CodeEditorPanels containerRef={containerRef} codeEditorPanelData={codeEditorPanelData} />
</div>
</CodeEditorProvider>
);
Expand Down
Loading
Loading