Skip to content

Commit 60828e0

Browse files
committed
Scope workspace zoom to focused view
1 parent 3a985c1 commit 60828e0

6 files changed

Lines changed: 167 additions & 33 deletions

File tree

App.tsx

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,12 @@ export const MainApp: React.FC = () => {
230230
const [isRestoringActiveDocument, setIsRestoringActiveDocument] = useState(true);
231231
const [hasRestoredActiveDocument, setHasRestoredActiveDocument] = useState(false);
232232
const [previewScale, setPreviewScale] = useState(PREVIEW_INITIAL_SCALE);
233+
const [editorScale, setEditorScale] = useState(PREVIEW_INITIAL_SCALE);
233234
const [previewResetSignal, setPreviewResetSignal] = useState(0);
234235
const [isPreviewVisible, setIsPreviewVisible] = useState(false);
235236
const [isPreviewZoomReady, setIsPreviewZoomReady] = useState(false);
236237
const [previewMetadata, setPreviewMetadata] = useState<PreviewMetadata | null>(null);
238+
const [workspaceZoomTarget, setWorkspaceZoomTarget] = useState<'preview' | 'editor'>('editor');
237239

238240
useEffect(() => {
239241
if (!isPreviewVisible) {
@@ -251,25 +253,41 @@ export const MainApp: React.FC = () => {
251253
const openDocumentIds = tabState.order;
252254
const storedActiveDocumentIdRef = useRef<string | null>(null);
253255

254-
const clampPreviewScale = useCallback((value: number) => {
256+
const clampZoomScale = useCallback((value: number) => {
255257
return Math.min(Math.max(value, PREVIEW_MIN_SCALE), PREVIEW_MAX_SCALE);
256258
}, []);
257259

258260
const handlePreviewScaleChange = useCallback((value: number) => {
259-
setPreviewScale(clampPreviewScale(value));
260-
}, [clampPreviewScale]);
261+
setPreviewScale(clampZoomScale(value));
262+
}, [clampZoomScale]);
261263

262264
const handlePreviewZoomIn = useCallback(() => {
263-
setPreviewScale(prev => clampPreviewScale(prev * (1 + PREVIEW_ZOOM_STEP)));
264-
}, [clampPreviewScale]);
265+
if (workspaceZoomTarget === 'preview') {
266+
setPreviewScale(prev => clampZoomScale(prev * (1 + PREVIEW_ZOOM_STEP)));
267+
} else {
268+
setEditorScale(prev => clampZoomScale(prev * (1 + PREVIEW_ZOOM_STEP)));
269+
}
270+
}, [clampZoomScale, workspaceZoomTarget]);
265271

266272
const handlePreviewZoomOut = useCallback(() => {
267-
setPreviewScale(prev => clampPreviewScale(prev / (1 + PREVIEW_ZOOM_STEP)));
268-
}, [clampPreviewScale]);
273+
if (workspaceZoomTarget === 'preview') {
274+
setPreviewScale(prev => clampZoomScale(prev / (1 + PREVIEW_ZOOM_STEP)));
275+
} else {
276+
setEditorScale(prev => clampZoomScale(prev / (1 + PREVIEW_ZOOM_STEP)));
277+
}
278+
}, [clampZoomScale, workspaceZoomTarget]);
269279

270280
const handlePreviewReset = useCallback(() => {
271-
setPreviewScale(PREVIEW_INITIAL_SCALE);
272-
setPreviewResetSignal(prev => prev + 1);
281+
if (workspaceZoomTarget === 'preview') {
282+
setPreviewScale(PREVIEW_INITIAL_SCALE);
283+
setPreviewResetSignal(prev => prev + 1);
284+
} else {
285+
setEditorScale(PREVIEW_INITIAL_SCALE);
286+
}
287+
}, [workspaceZoomTarget]);
288+
289+
const handleWorkspaceZoomTargetChange = useCallback((target: 'preview' | 'editor') => {
290+
setWorkspaceZoomTarget(target);
273291
}, []);
274292

275293
const activateDocumentTab = useCallback((documentId: string) => {
@@ -482,11 +500,39 @@ export const MainApp: React.FC = () => {
482500
}
483501
}, [activeDocument]);
484502

503+
const previewZoomAvailable = useMemo(() => {
504+
if (view === 'info') {
505+
return true;
506+
}
507+
return isPreviewVisible && isPreviewZoomReady;
508+
}, [isPreviewVisible, isPreviewZoomReady, view]);
509+
510+
const editorZoomAvailable = useMemo(() => {
511+
return view === 'editor' && documentView === 'editor' && Boolean(activeDocument);
512+
}, [activeDocument, documentView, view]);
513+
514+
useEffect(() => {
515+
if (workspaceZoomTarget === 'preview' && !previewZoomAvailable && editorZoomAvailable) {
516+
setWorkspaceZoomTarget('editor');
517+
} else if (workspaceZoomTarget === 'editor' && !editorZoomAvailable && previewZoomAvailable) {
518+
setWorkspaceZoomTarget('preview');
519+
}
520+
}, [editorZoomAvailable, previewZoomAvailable, workspaceZoomTarget]);
521+
522+
const isWorkspaceZoomAvailable = useMemo(() => {
523+
return workspaceZoomTarget === 'preview' ? previewZoomAvailable : editorZoomAvailable;
524+
}, [editorZoomAvailable, previewZoomAvailable, workspaceZoomTarget]);
525+
526+
const workspaceZoomScale = useMemo(() => {
527+
return workspaceZoomTarget === 'preview' ? previewScale : editorScale;
528+
}, [editorScale, previewScale, workspaceZoomTarget]);
529+
485530
const documentItems = useMemo(() => items.filter(item => item.type === 'document'), [items]);
486531
const activeDocumentId = activeDocument?.id ?? null;
487532

488533
useEffect(() => {
489534
setPreviewScale(PREVIEW_INITIAL_SCALE);
535+
setEditorScale(PREVIEW_INITIAL_SCALE);
490536
setPreviewResetSignal(prev => prev + 1);
491537
}, [activeNode?.id, activeNode?.type]);
492538

@@ -2779,7 +2825,7 @@ export const MainApp: React.FC = () => {
27792825
return (
27802826
<InfoView
27812827
settings={settings}
2782-
previewScale={previewScale}
2828+
previewScale={workspaceZoomScale}
27832829
onPreviewScaleChange={handlePreviewScaleChange}
27842830
previewZoomOptions={{
27852831
minScale: PREVIEW_MIN_SCALE,
@@ -2790,6 +2836,7 @@ export const MainApp: React.FC = () => {
27902836
previewResetSignal={previewResetSignal}
27912837
onPreviewVisibilityChange={setIsPreviewVisible}
27922838
onPreviewZoomAvailabilityChange={setIsPreviewZoomReady}
2839+
onZoomTargetChange={handleWorkspaceZoomTargetChange}
27932840
/>
27942841
);
27952842
}
@@ -2829,6 +2876,7 @@ export const MainApp: React.FC = () => {
28292876
onToggleLock={(locked) => handleSetNodeLockState(activeNode.id, locked)}
28302877
formatTrigger={formatTrigger}
28312878
previewScale={previewScale}
2879+
editorScale={editorScale}
28322880
onPreviewScaleChange={handlePreviewScaleChange}
28332881
previewMinScale={PREVIEW_MIN_SCALE}
28342882
previewMaxScale={PREVIEW_MAX_SCALE}
@@ -2838,6 +2886,7 @@ export const MainApp: React.FC = () => {
28382886
onPreviewVisibilityChange={setIsPreviewVisible}
28392887
onPreviewZoomAvailabilityChange={setIsPreviewZoomReady}
28402888
onPreviewMetadataChange={setPreviewMetadata}
2889+
onZoomTargetChange={handleWorkspaceZoomTargetChange}
28412890
/>
28422891
);
28432892
}
@@ -3022,15 +3071,16 @@ export const MainApp: React.FC = () => {
30223071
databaseStatus={databaseStatus}
30233072
onDatabaseMenu={handleDatabaseMenu}
30243073
onOpenAbout={handleOpenAbout}
3025-
previewScale={previewScale}
3074+
previewScale={workspaceZoomScale}
30263075
onPreviewZoomIn={handlePreviewZoomIn}
30273076
onPreviewZoomOut={handlePreviewZoomOut}
30283077
onPreviewReset={handlePreviewReset}
3029-
isPreviewZoomAvailable={isPreviewVisible && isPreviewZoomReady}
3078+
isPreviewZoomAvailable={isWorkspaceZoomAvailable}
30303079
previewMinScale={PREVIEW_MIN_SCALE}
30313080
previewMaxScale={PREVIEW_MAX_SCALE}
30323081
previewInitialScale={PREVIEW_INITIAL_SCALE}
30333082
previewMetadata={previewMetadata}
3083+
zoomTarget={workspaceZoomTarget}
30343084
/>
30353085
</div>
30363086

components/CodeEditor.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface CodeEditorProps {
2121
activeLineHighlightColorLight?: string;
2222
activeLineHighlightColorDark?: string;
2323
readOnly?: boolean;
24+
onFocusChange?: (hasFocus: boolean) => void;
2425
}
2526

2627
export interface CodeEditorHandle {
@@ -118,14 +119,16 @@ const toMonacoKeybinding = (monacoApi: any, keys: string[]): number | null => {
118119
return keybinding | primaryKey;
119120
};
120121

121-
const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, language, onChange, onScroll, customShortcuts = {}, fontFamily, fontSize, activeLineHighlightColorLight, activeLineHighlightColorDark, readOnly = false }, ref) => {
122+
const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, language, onChange, onScroll, customShortcuts = {}, fontFamily, fontSize, activeLineHighlightColorLight, activeLineHighlightColorDark, readOnly = false, onFocusChange }, ref) => {
122123
const editorRef = useRef<HTMLDivElement>(null);
123124
const monacoInstanceRef = useRef<any>(null);
124125
const monacoApiRef = useRef<any>(null);
125126
const { theme } = useTheme();
126127
const contentRef = useRef(content);
127128
const customShortcutsRef = useRef<Record<string, string[]>>({});
128129
const actionDisposablesRef = useRef<Array<{ dispose: () => void }>>([]);
130+
const focusDisposableRef = useRef<{ dispose: () => void } | null>(null);
131+
const blurDisposableRef = useRef<{ dispose: () => void } | null>(null);
129132
const computedFontFamily = useMemo(() => {
130133
const candidate = (fontFamily ?? '').trim();
131134
return candidate || DEFAULT_SETTINGS.editorFontFamily;
@@ -202,6 +205,13 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
202205
actionDisposablesRef.current = [];
203206
}, []);
204207

208+
const disposeFocusListeners = useCallback(() => {
209+
focusDisposableRef.current?.dispose();
210+
focusDisposableRef.current = null;
211+
blurDisposableRef.current?.dispose();
212+
blurDisposableRef.current = null;
213+
}, []);
214+
205215
const applyEditorShortcuts = useCallback(() => {
206216
const monacoApi = monacoApiRef.current;
207217
if (!monacoInstanceRef.current || !monacoApi) {
@@ -267,6 +277,7 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
267277

268278
if (monacoInstanceRef.current) {
269279
disposeEditorShortcuts();
280+
disposeFocusListeners();
270281
monacoInstanceRef.current.dispose();
271282
}
272283

@@ -309,6 +320,16 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
309320
}
310321
});
311322

323+
disposeFocusListeners();
324+
if (onFocusChange) {
325+
focusDisposableRef.current = editorInstance.onDidFocusEditorWidget(() => {
326+
onFocusChange(true);
327+
});
328+
blurDisposableRef.current = editorInstance.onDidBlurEditorWidget(() => {
329+
onFocusChange(false);
330+
});
331+
}
332+
312333
monacoInstanceRef.current = editorInstance;
313334
applyEditorShortcuts();
314335
} catch (error) {
@@ -322,13 +343,14 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
322343
return () => {
323344
isCancelled = true;
324345
disposeEditorShortcuts();
346+
disposeFocusListeners();
325347
if (monacoInstanceRef.current) {
326348
monacoInstanceRef.current.dispose();
327349
monacoInstanceRef.current = null;
328350
}
329351
monacoApiRef.current = null;
330352
};
331-
}, [onChange, onScroll, applyEditorShortcuts, disposeEditorShortcuts, computedFontFamily, computedFontSize, readOnly]);
353+
}, [onChange, onScroll, applyEditorShortcuts, disposeEditorShortcuts, disposeFocusListeners, computedFontFamily, computedFontSize, readOnly, onFocusChange]);
332354

333355
// Effect to update content from props if it changes externally
334356
useEffect(() => {

components/InfoView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface InfoViewProps {
2626
previewResetSignal?: number;
2727
onPreviewVisibilityChange?: (isVisible: boolean) => void;
2828
onPreviewZoomAvailabilityChange?: (isAvailable: boolean) => void;
29+
onZoomTargetChange?: (target: 'preview' | 'editor') => void;
2930
}
3031

3132
const PreviewZoomAvailabilityReset: React.FC<{
@@ -46,6 +47,7 @@ const InfoView: React.FC<InfoViewProps> = ({
4647
previewResetSignal,
4748
onPreviewVisibilityChange,
4849
onPreviewZoomAvailabilityChange,
50+
onZoomTargetChange,
4951
}) => {
5052
const [activeTab, setActiveTab] = useState<DocTab>('Readme');
5153
const [documents, setDocuments] = useState<Record<DocTab, string>>({
@@ -109,11 +111,12 @@ const InfoView: React.FC<InfoViewProps> = ({
109111

110112
useEffect(() => {
111113
onPreviewVisibilityChange?.(true);
114+
onZoomTargetChange?.('preview');
112115
return () => {
113116
onPreviewVisibilityChange?.(false);
114117
onPreviewZoomAvailabilityChange?.(false);
115118
};
116-
}, [onPreviewVisibilityChange, onPreviewZoomAvailabilityChange]);
119+
}, [onPreviewVisibilityChange, onPreviewZoomAvailabilityChange, onZoomTargetChange]);
117120

118121
const activeTabContent = documents[activeTab];
119122
const isLoadingActiveTab = activeTabContent === 'Loading...';

components/MonacoDiffEditor.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,19 @@ interface MonacoDiffEditorProps {
1919
fontSize?: number;
2020
activeLineHighlightColorLight?: string;
2121
activeLineHighlightColorDark?: string;
22+
onFocusChange?: (hasFocus: boolean) => void;
2223
}
2324

24-
const MonacoDiffEditor: React.FC<MonacoDiffEditorProps> = ({ oldText, newText, language, renderMode = 'side-by-side', readOnly = false, onChange, onScroll, fontFamily, fontSize, activeLineHighlightColorLight, activeLineHighlightColorDark }) => {
25+
const MonacoDiffEditor: React.FC<MonacoDiffEditorProps> = ({ oldText, newText, language, renderMode = 'side-by-side', readOnly = false, onChange, onScroll, fontFamily, fontSize, activeLineHighlightColorLight, activeLineHighlightColorDark, onFocusChange }) => {
2526
const editorRef = useRef<HTMLDivElement>(null);
2627
const editorInstanceRef = useRef<any>(null);
2728
const monacoApiRef = useRef<any>(null);
2829
const { theme } = useTheme();
2930
const modelsRef = useRef<{ original: any; modified: any } | null>(null);
3031
const changeListenerRef = useRef<{ dispose: () => void } | null>(null);
3132
const scrollListenerRef = useRef<{ dispose: () => void } | null>(null);
33+
const focusListenerRef = useRef<{ dispose: () => void } | null>(null);
34+
const blurListenerRef = useRef<{ dispose: () => void } | null>(null);
3235
const computedFontFamily = useMemo(() => {
3336
const candidate = (fontFamily ?? '').trim();
3437
return candidate || DEFAULT_SETTINGS.editorFontFamily;
@@ -73,6 +76,10 @@ const MonacoDiffEditor: React.FC<MonacoDiffEditorProps> = ({ oldText, newText, l
7376
scrollListenerRef.current.dispose();
7477
scrollListenerRef.current = null;
7578
}
79+
focusListenerRef.current?.dispose();
80+
focusListenerRef.current = null;
81+
blurListenerRef.current?.dispose();
82+
blurListenerRef.current = null;
7683
}, []);
7784

7885
useEffect(() => {
@@ -153,6 +160,15 @@ const MonacoDiffEditor: React.FC<MonacoDiffEditorProps> = ({ oldText, newText, l
153160
});
154161
});
155162
}
163+
164+
if (onFocusChange) {
165+
focusListenerRef.current = modifiedEditor.onDidFocusEditorWidget(() => {
166+
onFocusChange(true);
167+
});
168+
blurListenerRef.current = modifiedEditor.onDidBlurEditorWidget(() => {
169+
onFocusChange(false);
170+
});
171+
}
156172
} catch (error) {
157173
// eslint-disable-next-line no-console
158174
console.error('Failed to initialize Monaco diff editor', error);

0 commit comments

Comments
 (0)