feat: highlight focused regions

adds a region wrapper with a highlight effect when that region is focused, this behavior can be toggled as a setting
This commit is contained in:
joshistoast
2025-03-13 20:55:08 -06:00
committed by psychedelicious
parent 09bf7c35eb
commit 02b91e8e7b
11 changed files with 179 additions and 111 deletions

View File

@@ -1,28 +1,28 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo, useRef } from 'react';
import { memo } from 'react';
import { ParamDenoisingStrength } from './ParamDenoisingStrength';
export const CanvasLayersPanelContent = memo(() => {
const hasEntities = useAppSelector(selectHasEntities);
const layersPanelFocusRef = useRef<HTMLDivElement>(null);
useFocusRegion('layers', layersPanelFocusRef);
return (
<Flex ref={layersPanelFocusRef} flexDir="column" gap={2} w="full" h="full">
<EntityListSelectedEntityActionBar />
<Divider py={0} />
<ParamDenoisingStrength />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Flex>
<FocusRegionWrapper region="layers" w="full" h="full">
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListSelectedEntityActionBar />
<Divider py={0} />
<ParamDenoisingStrength />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Flex>
</FocusRegionWrapper>
);
});

View File

@@ -1,6 +1,6 @@
import { ContextMenu, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
@@ -18,7 +18,7 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import { GatedImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { memo, useCallback, useRef } from 'react';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -35,7 +35,6 @@ const MenuContent = () => {
};
export const CanvasMainPanelContent = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
@@ -43,82 +42,81 @@ export const CanvasMainPanelContent = memo(() => {
return <MenuContent />;
}, []);
useFocusRegion('canvas', ref);
return (
<Flex
tabIndex={-1}
ref={ref}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<CanvasToolbar />
</CanvasManagerProviderGate>
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex
ref={ref}
position="relative"
w="full"
h="full"
bg={dynamicGrid ? 'base.850' : 'base.900'}
borderRadius="base"
overflow="hidden"
>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsSendingToGallery />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
<FocusRegionWrapper region="canvas" w="full" h="full">
<Flex
tabIndex={-1}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
<CanvasToolbar />
</CanvasManagerProviderGate>
</Flex>
<Flex position="absolute" bottom={4}>
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex
ref={ref}
position="relative"
w="full"
h="full"
bg={dynamicGrid ? 'base.850' : 'base.900'}
borderRadius="base"
overflow="hidden"
>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsSendingToGallery />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
<CanvasManagerProviderGate>
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
</CanvasManagerProviderGate>
</Flex>
<Flex position="absolute" bottom={4}>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
</CanvasManagerProviderGate>
</Flex>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
<CanvasDropArea />
</CanvasManagerProviderGate>
<GatedImageViewer />
</Flex>
<CanvasManagerProviderGate>
<CanvasDropArea />
</CanvasManagerProviderGate>
<GatedImageViewer />
</Flex>
</FocusRegionWrapper>
);
});

View File

@@ -1,6 +1,6 @@
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
@@ -26,8 +26,6 @@ const GalleryPanelContent = () => {
const dispatch = useAppDispatch();
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const galleryPanelFocusRef = useRef<HTMLDivElement>(null);
useFocusRegion('gallery', galleryPanelFocusRef);
const boardsListPanelOptions = useMemo<UsePanelOptions>(
() => ({
@@ -50,7 +48,7 @@ const GalleryPanelContent = () => {
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
return (
<Flex ref={galleryPanelFocusRef} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
<FocusRegionWrapper region="gallery" position="relative" flexDirection="column" h="full" w="full">
<Flex alignItems="center" justifyContent="space-between" w="full">
<Flex flexGrow={1} flexBasis={0}>
<Button
@@ -99,7 +97,7 @@ const GalleryPanelContent = () => {
<Gallery />
</Panel>
</PanelGroup>
</Flex>
</FocusRegionWrapper>
);
};

View File

@@ -1,6 +1,6 @@
import { Box, Flex, IconButton } from '@invoke-ai/ui-library';
import { Box, IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
@@ -9,7 +9,7 @@ import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewe
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { selectHasImageToCompare } from 'features/gallery/store/gallerySelectors';
import type { ReactNode } from 'react';
import { memo, useRef } from 'react';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
@@ -29,15 +29,12 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
useAssertSingleton('ImageViewer');
const hasImageToCompare = useAppSelector(selectHasImageToCompare);
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
const ref = useRef<HTMLDivElement>(null);
useFocusRegion('viewer', ref, useFocusRegionOptions);
return (
<Flex
ref={ref}
tabIndex={-1}
layerStyle="first"
borderRadius="base"
<FocusRegionWrapper
region="viewer"
w="full"
h="full"
position="absolute"
flexDirection="column"
top={0}
@@ -47,6 +44,8 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
alignItems="center"
justifyContent="center"
overflow="hidden"
layerStyle="first"
{...useFocusRegionOptions}
>
{hasImageToCompare && <CompareToolbar />}
{!hasImageToCompare && <ViewerToolbar closeButton={closeButton} />}
@@ -55,7 +54,7 @@ export const ImageViewer = memo(({ closeButton }: Props) => {
{hasImageToCompare && <ImageComparison containerDims={containerDims} />}
</Box>
<ImageComparisonDroppable />
</Flex>
</FocusRegionWrapper>
);
});

View File

@@ -1,10 +1,9 @@
import { Flex } from '@invoke-ai/ui-library';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { useFocusRegion } from 'common/hooks/focus';
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
import { memo, useRef } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFlowArrowBold } from 'react-icons/pi';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
@@ -16,13 +15,10 @@ import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
const NodeEditor = () => {
const { data, isLoading } = useGetOpenAPISchemaQuery();
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useFocusRegion('workflows', ref);
return (
<Flex
tabIndex={-1}
ref={ref}
<FocusRegionWrapper
region="workflows"
layerStyle="first"
position="relative"
width="full"
@@ -42,7 +38,7 @@ const NodeEditor = () => {
)}
<WorkflowEditorSettings />
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={PiFlowArrowBold} />}
</Flex>
</FocusRegionWrapper>
);
};

View File

@@ -30,6 +30,7 @@ import {
selectSystemShouldAntialiasProgressImage,
selectSystemShouldConfirmOnDelete,
selectSystemShouldConfirmOnNewSession,
selectSystemShouldEnableHighlightFocusedRegions,
selectSystemShouldEnableInformationalPopovers,
selectSystemShouldEnableModelDescriptions,
selectSystemShouldShowInvocationProgressDetail,
@@ -38,6 +39,7 @@ import {
setShouldConfirmOnDelete,
setShouldEnableInformationalPopovers,
setShouldEnableModelDescriptions,
setShouldHighlightFocusedRegions,
setShouldShowInvocationProgressDetail,
shouldAntialiasProgressImageChanged,
shouldConfirmOnNewSessionToggled,
@@ -106,6 +108,7 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps)
const shouldUseWatermarker = useAppSelector(selectSystemShouldUseWatermarker);
const shouldEnableInformationalPopovers = useAppSelector(selectSystemShouldEnableInformationalPopovers);
const shouldEnableModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
const shouldHighlightFocusedRegions = useAppSelector(selectSystemShouldEnableHighlightFocusedRegions);
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
const shouldShowInvocationProgressDetail = useAppSelector(selectSystemShouldShowInvocationProgressDetail);
const onToggleConfirmOnNewSession = useCallback(() => {
@@ -182,6 +185,13 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps)
[dispatch]
);
const handleChangeShouldHighlightFocusedRegions = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(setShouldHighlightFocusedRegions(e.target.checked));
},
[dispatch]
);
return (
<>
{cloneElement(children, {
@@ -263,6 +273,13 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps)
onChange={handleChangeShouldEnableModelDescriptions}
/>
</FormControl>
<FormControl>
<FormLabel>{t('settings.enableHighlightFocusedRegions')}</FormLabel>
<Switch
isChecked={shouldHighlightFocusedRegions}
onChange={handleChangeShouldHighlightFocusedRegions}
/>
</FormControl>
</StickyScrollable>
{Boolean(config?.shouldShowDeveloperSettings) && (

View File

@@ -22,6 +22,7 @@ const initialSystemState: SystemState = {
logLevel: 'debug',
logNamespaces: [...zLogNamespace.options],
shouldShowInvocationProgressDetail: false,
shouldHighlightFocusedRegions: true,
};
export const systemSlice = createSlice({
@@ -68,6 +69,9 @@ export const systemSlice = createSlice({
setShouldShowInvocationProgressDetail(state, action: PayloadAction<boolean>) {
state.shouldShowInvocationProgressDetail = action.payload;
},
setShouldHighlightFocusedRegions(state, action: PayloadAction<boolean>) {
state.shouldHighlightFocusedRegions = action.payload;
},
},
});
@@ -84,6 +88,7 @@ export const {
setShouldEnableModelDescriptions,
shouldConfirmOnNewSessionToggled,
setShouldShowInvocationProgressDetail,
setShouldHighlightFocusedRegions,
} = systemSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -121,6 +126,9 @@ export const selectSystemShouldEnableInformationalPopovers = createSystemSelecto
export const selectSystemShouldEnableModelDescriptions = createSystemSelector(
(system) => system.shouldEnableModelDescriptions
);
export const selectSystemShouldEnableHighlightFocusedRegions = createSystemSelector(
(system) => system.shouldHighlightFocusedRegions
);
export const selectSystemShouldConfirmOnNewSession = createSystemSelector((system) => system.shouldConfirmOnNewSession);
export const selectSystemShouldShowInvocationProgressDetail = createSystemSelector(
(system) => system.shouldShowInvocationProgressDetail

View File

@@ -43,4 +43,5 @@ export interface SystemState {
logLevel: LogLevel;
logNamespaces: LogNamespace[];
shouldShowInvocationProgressDetail: boolean;
shouldHighlightFocusedRegions: boolean;
}