Skip to content

Commit 6cf03b9

Browse files
feat(ui): implement generalized textarea size tracking system
1 parent 621c1a8 commit 6cf03b9

File tree

3 files changed

+121
-11
lines changed

3 files changed

+121
-11
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useAppStore } from 'app/store/nanostores/store';
2+
import type { Dimensions } from 'features/controlLayers/store/types';
3+
import { selectUiSlice, textAreaSizesStateChanged } from 'features/ui/store/uiSlice';
4+
import { debounce } from 'lodash-es';
5+
import { type RefObject, useCallback, useEffect, useMemo } from 'react';
6+
7+
type Options = {
8+
trackWidth: boolean;
9+
trackHeight: boolean;
10+
initialWidth?: number;
11+
initialHeight?: number;
12+
};
13+
14+
/**
15+
* Persists the width and/or height of a text area to redux.
16+
* @param id The unique id of this textarea, used as key to storage
17+
* @param ref A ref to the textarea element
18+
* @param options.trackWidth Whether to track width
19+
* @param options.trackHeight Whether to track width
20+
* @param options.initialWidth An optional initial width in pixels
21+
* @param options.initialHeight An optional initial height in pixels
22+
*/
23+
export const usePersistedTextAreaSize = (id: string, ref: RefObject<HTMLTextAreaElement>, options: Options) => {
24+
const { dispatch, getState } = useAppStore();
25+
26+
const onResize = useCallback(
27+
(size: Partial<Dimensions>) => {
28+
dispatch(textAreaSizesStateChanged({ id, size }));
29+
},
30+
[dispatch, id]
31+
);
32+
33+
const debouncedOnResize = useMemo(() => debounce(onResize, 300), [onResize]);
34+
35+
useEffect(() => {
36+
const el = ref.current;
37+
if (!el) {
38+
return;
39+
}
40+
41+
// Nothing to do here if we are not tracking anything.
42+
if (!options.trackHeight && !options.trackWidth) {
43+
return;
44+
}
45+
46+
// Before registering the observer, grab the stored size from state - we may need to restore the size.
47+
const storedSize = selectUiSlice(getState()).textAreaSizes[id];
48+
49+
// Prefer to restore the stored size, falling back to initial size if it exists
50+
if (storedSize?.width !== undefined) {
51+
el.style.width = `${storedSize.width}px`;
52+
} else if (options.initialWidth !== undefined) {
53+
el.style.width = `${options.initialWidth}px`;
54+
}
55+
56+
if (storedSize?.height !== undefined) {
57+
el.style.height = `${storedSize.height}px`;
58+
} else if (options.initialHeight !== undefined) {
59+
el.style.height = `${options.initialHeight}px`;
60+
}
61+
62+
let currentHeight = el.offsetHeight;
63+
let currentWidth = el.offsetWidth;
64+
65+
const resizeObserver = new ResizeObserver(() => {
66+
// We only want to push the changes if a tracked dimension changes
67+
let didChange = false;
68+
const newSize: Partial<Dimensions> = {};
69+
70+
if (options.trackHeight) {
71+
if (el.offsetHeight !== currentHeight) {
72+
didChange = true;
73+
currentHeight = el.offsetHeight;
74+
}
75+
newSize.height = currentHeight;
76+
}
77+
78+
if (options.trackWidth) {
79+
if (el.offsetWidth !== currentWidth) {
80+
didChange = true;
81+
currentWidth = el.offsetWidth;
82+
}
83+
newSize.width = currentWidth;
84+
}
85+
86+
if (didChange) {
87+
debouncedOnResize(newSize);
88+
}
89+
});
90+
91+
resizeObserver.observe(el);
92+
93+
return () => {
94+
debouncedOnResize.cancel();
95+
resizeObserver.disconnect();
96+
};
97+
}, [
98+
debouncedOnResize,
99+
dispatch,
100+
getState,
101+
id,
102+
options.initialHeight,
103+
options.initialWidth,
104+
options.trackHeight,
105+
options.trackWidth,
106+
ref,
107+
]);
108+
};

invokeai/frontend/web/src/features/ui/store/uiSlice.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
22
import { createSelector, createSlice } from '@reduxjs/toolkit';
33
import type { PersistConfig, RootState } from 'app/store/store';
44
import { newSessionRequested } from 'features/controlLayers/store/actions';
5+
import type { Dimensions } from 'features/controlLayers/store/types';
56
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
67
import { atom } from 'nanostores';
78

@@ -15,8 +16,8 @@ const initialUIState: UIState = {
1516
shouldShowProgressInViewer: true,
1617
accordions: {},
1718
expanders: {},
19+
textAreaSizes: {},
1820
shouldShowNotificationV2: true,
19-
positivePromptBoxHeight: 40,
2021
};
2122

2223
export const uiSlice = createSlice({
@@ -43,12 +44,13 @@ export const uiSlice = createSlice({
4344
const { id, isOpen } = action.payload;
4445
state.expanders[id] = isOpen;
4546
},
47+
textAreaSizesStateChanged: (state, action: PayloadAction<{ id: string; size: Partial<Dimensions> }>) => {
48+
const { id, size } = action.payload;
49+
state.textAreaSizes[id] = size;
50+
},
4651
shouldShowNotificationChanged: (state, action: PayloadAction<boolean>) => {
4752
state.shouldShowNotificationV2 = action.payload;
4853
},
49-
positivePromptBoxHeightChanged: (state, action: PayloadAction<number>) => {
50-
state.positivePromptBoxHeight = action.payload;
51-
},
5254
},
5355
extraReducers(builder) {
5456
builder.addCase(workflowLoaded, (state) => {
@@ -68,7 +70,7 @@ export const {
6870
accordionStateChanged,
6971
expanderStateChanged,
7072
shouldShowNotificationChanged,
71-
positivePromptBoxHeightChanged,
73+
textAreaSizesStateChanged,
7274
} = uiSlice.actions;
7375

7476
export const selectUiSlice = (state: RootState) => state.ui;
@@ -105,5 +107,3 @@ const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows'] as
105107
export const RIGHT_PANEL_MIN_SIZE_PX = 390;
106108
export const $isRightPanelOpen = atom(true);
107109
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));
108-
109-
export const selectPositivePromptBoxHeight = createSelector(selectUiSlice, (ui) => ui.positivePromptBoxHeight);

invokeai/frontend/web/src/features/ui/store/uiTypes.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Dimensions } from 'features/controlLayers/store/types';
2+
13
export type TabName = 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue';
24
export type CanvasRightPanelTabName = 'layers' | 'gallery';
35

@@ -31,11 +33,11 @@ export interface UIState {
3133
*/
3234
expanders: Record<string, boolean>;
3335
/**
34-
* Whether or not to show the user the open notification. Bump version to reset users who may have closed previous version.
36+
* The size of textareas. The key is the id of the text area, and the value is an object representing its width and/or height.
3537
*/
36-
shouldShowNotificationV2: boolean;
38+
textAreaSizes: Record<string, Partial<Dimensions>>;
3739
/**
38-
* The height of the positive prompt box.
40+
* Whether or not to show the user the open notification. Bump version to reset users who may have closed previous version.
3941
*/
40-
positivePromptBoxHeight: number;
42+
shouldShowNotificationV2: boolean;
4143
}

0 commit comments

Comments
 (0)