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

fix(ui): misc fixes #7252

Merged
merged 5 commits into from
Nov 4, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { CSSProperties } from 'react';

/**
* Chakra's Tooltip's method of finding the nearest scroll parent has a problem - it assumes the first parent with
* `overflow: hidden` is the scroll parent. In this case, the Collapse component has that style, but isn't scrollable
* itself. The result is that the tooltip does not close on scroll, because the scrolling happens higher up in the DOM.
*
* As a hacky workaround, we can set the overflow to `visible`, which allows the scroll parent search to continue up to
* the actual scroll parent (in this case, the OverlayScrollbarsComponent in BoardsListWrapper).
*
* See: https://github.com/chakra-ui/chakra-ui/issues/7871#issuecomment-2453780958
*/
export const fixTooltipCloseOnScrollStyles: CSSProperties = {
overflow: 'visible',
};
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,16 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit

const result = await withResultAsync(() => {
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
return canvasManager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
is_intermediate: !saveToGallery,
metadata,
});
return canvasManager.compositor.getCompositeImageDTO(
rasterAdapters,
rect,
{
is_intermediate: !saveToGallery,
metadata,
},
undefined,
true // force upload the image to ensure it gets added to the gallery
);
});

if (result.isOk()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,18 +253,20 @@ export class CanvasCompositorModule extends CanvasModuleBase {
* @param rect The region to include in the rasterized image
* @param uploadOptions Options for uploading the image
* @param compositingOptions Options for compositing the entities
* @param forceUpload If true, the image is always re-uploaded, returning a new image DTO
* @returns A promise that resolves to the image DTO
*/
getCompositeImageDTO = async (
adapters: CanvasEntityAdapter[],
rect: Rect,
uploadOptions: Pick<UploadOptions, 'is_intermediate' | 'metadata'>,
compositingOptions?: CompositingOptions
compositingOptions?: CompositingOptions,
forceUpload?: boolean
): Promise<ImageDTO> => {
assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect');

const hash = this.getCompositeHash(adapters, { rect });
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
const cachedImageName = forceUpload ? undefined : this.manager.cache.imageNameCache.get(hash);

let imageDTO: ImageDTO | null = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
* Processes the filter, updating the module's state and rendering the filtered image.
*/
processImmediate = async () => {
if (!this.$isFiltering.get()) {
this.log.warn('Cannot process filter when not initialized');
return;
}
const config = this.$filterConfig.get();
const filterData = IMAGE_FILTERS[config.type];

Expand Down Expand Up @@ -342,7 +346,6 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
});

// Final cleanup and teardown, returning user to main canvas UI
this.resetEphemeralState();
this.teardown();
};

Expand Down Expand Up @@ -409,9 +412,11 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
};

teardown = () => {
this.$initialFilterConfig.set(null);
this.konva.group.remove();
this.unsubscribe();
this.konva.group.remove();
// The reset must be done _after_ unsubscribing from listeners, in case the listeners would otherwise react to
// the reset. For example, if auto-processing is enabled and we reset the state, it may trigger processing.
this.resetEphemeralState();
this.$isFiltering.set(false);
this.manager.stateApi.$filteringAdapter.set(null);
};
Expand All @@ -428,7 +433,6 @@ export class CanvasEntityFilterer extends CanvasModuleBase {

cancel = () => {
this.log.trace('Canceling');
this.resetEphemeralState();
this.teardown();
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Mutex } from 'async-mutex';
import { parseify } from 'common/util/serialize';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util';
Expand Down Expand Up @@ -26,13 +27,13 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
group: Konva.Group;
image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
};
isLoading: boolean = false;
isError: boolean = false;
$isLoading = atom<boolean>(false);
$isError = atom<boolean>(false);
imageElement: HTMLImageElement | null = null;

subscriptions = new Set<() => void>();
$lastProgressEvent = atom<ProgressEventWithImage | null>(null);
hasActiveGeneration: boolean = false;
$hasActiveGeneration = atom<boolean>(false);
mutex: Mutex = new Mutex();

constructor(manager: CanvasManager) {
Expand All @@ -56,12 +57,9 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
this.subscriptions.add(
this.manager.stateApi.createStoreSubscription(selectCanvasQueueCounts, ({ data }) => {
if (data && (data.in_progress > 0 || data.pending > 0)) {
this.hasActiveGeneration = true;
this.$hasActiveGeneration.set(true);
} else {
this.hasActiveGeneration = false;
if (!this.manager.stagingArea.$isStaging.get()) {
this.$lastProgressEvent.set(null);
}
this.$hasActiveGeneration.set(false);
}
})
);
Expand All @@ -76,23 +74,36 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
if (!isProgressEventWithImage(data)) {
return;
}
if (!this.hasActiveGeneration) {
if (!this.$hasActiveGeneration.get()) {
return;
}
this.$lastProgressEvent.set(data);
};

// Handle a canceled or failed canvas generation. We should clear the progress image in this case.
const queueItemStatusChangedListener = (data: S['QueueItemStatusChangedEvent']) => {
if (data.destination !== 'canvas') {
return;
}
if (data.status === 'failed' || data.status === 'canceled') {
this.$lastProgressEvent.set(null);
this.$hasActiveGeneration.set(false);
}
};

const clearProgress = () => {
this.$lastProgressEvent.set(null);
};

this.manager.socket.on('invocation_progress', progressListener);
this.manager.socket.on('queue_item_status_changed', queueItemStatusChangedListener);
this.manager.socket.on('connect', clearProgress);
this.manager.socket.on('connect_error', clearProgress);
this.manager.socket.on('disconnect', clearProgress);

return () => {
this.manager.socket.off('invocation_progress', progressListener);
this.manager.socket.off('queue_item_status_changed', queueItemStatusChangedListener);
this.manager.socket.off('connect', clearProgress);
this.manager.socket.off('connect_error', clearProgress);
this.manager.socket.off('disconnect', clearProgress);
Expand All @@ -114,13 +125,13 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
this.konva.image?.destroy();
this.konva.image = null;
this.imageElement = null;
this.isLoading = false;
this.isError = false;
this.$isLoading.set(false);
this.$isError.set(false);
release();
return;
}

this.isLoading = true;
this.$isLoading.set(true);

const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
try {
Expand Down Expand Up @@ -149,9 +160,9 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
// Should not be visible if the user has disabled showing staging images
this.konva.group.visible(this.manager.stagingArea.$shouldShowStagedImage.get());
} catch {
this.isError = true;
this.$isError.set(true);
} finally {
this.isLoading = false;
this.$isLoading.set(false);
release();
}
};
Expand All @@ -162,4 +173,16 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
this.subscriptions.clear();
this.konva.group.destroy();
};

repr = () => {
return {
id: this.id,
type: this.type,
path: this.path,
$lastProgressEvent: parseify(this.$lastProgressEvent.get()),
$hasActiveGeneration: this.$hasActiveGeneration.get(),
$isError: this.$isError.get(),
$isLoading: this.$isLoading.get(),
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,11 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
* Processes the SAM points to segment the entity, updating the module's state and rendering the mask.
*/
processImmediate = async () => {
if (!this.$isSegmenting.get()) {
this.log.warn('Cannot process segmentation when not initialized');
return;
}

if (this.$isProcessing.get()) {
this.log.warn('Already processing');
return;
Expand Down Expand Up @@ -689,7 +694,6 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
});

// Final cleanup and teardown, returning user to main canvas UI
this.resetEphemeralState();
this.teardown();
};

Expand Down Expand Up @@ -758,7 +762,6 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
cancel = () => {
this.log.trace('Canceling');
// Reset the module's state and tear down, returning user to main canvas UI
this.resetEphemeralState();
this.teardown();
};

Expand All @@ -773,8 +776,11 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
* - Resets the global segmenting adapter
*/
teardown = () => {
this.konva.group.remove();
this.unsubscribe();
this.konva.group.remove();
// The reset must be done _after_ unsubscribing from listeners, in case the listeners would otherwise react to
// the reset. For example, if auto-processing is enabled and we reset the state, it may trigger processing.
this.resetEphemeralState();
this.$isSegmenting.set(false);
this.manager.stateApi.$segmentingAdapter.set(null);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles';
import {
selectBoardSearchText,
selectListBoardsQueryArgs,
Expand Down Expand Up @@ -104,7 +105,7 @@ export const BoardsList = ({ isPrivate }: Props) => {
)}
<AddBoardButton isPrivateBoard={isPrivate} />
</Flex>
<Collapse in={isOpen}>
<Collapse in={isOpen} style={fixTooltipCloseOnScrollStyles}>
<Flex direction="column" gap={1}>
{boardElements.length ? (
boardElements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Button, Collapse, Flex, Icon, Spinner, Text } from '@invoke-ai/ui-libra
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles';
import { useCategorySections } from 'features/nodes/hooks/useCategorySections';
import {
selectWorkflowOrderBy,
Expand Down Expand Up @@ -61,7 +62,7 @@ export const WorkflowList = ({ category }: { category: WorkflowCategory }) => {
</Text>
</Flex>
</Button>
<Collapse in={isOpen}>
<Collapse in={isOpen} style={fixTooltipCloseOnScrollStyles}>
{isLoading ? (
<Flex alignItems="center" justifyContent="center" p={20}>
<Spinner />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const WorkflowListItem = ({ workflow }: { workflow: WorkflowRecordListIte
onMouseOut={handleMouseOut}
alignItems="center"
>
<Tooltip label={<WorkflowListItemTooltip workflow={workflow} />}>
<Tooltip label={<WorkflowListItemTooltip workflow={workflow} />} closeOnScroll>
<Flex flexDir="column" gap={1}>
<Flex gap={4} alignItems="center">
<Text noOfLines={2}>{workflow.name}</Text>
Expand Down Expand Up @@ -137,6 +137,7 @@ export const WorkflowListItem = ({ workflow }: { workflow: WorkflowRecordListIte
label={t('workflows.edit')}
// This prevents an issue where the tooltip isn't closed after the modal is opened
isOpen={!isHovered ? false : undefined}
closeOnScroll
>
<IconButton
size="sm"
Expand All @@ -150,6 +151,7 @@ export const WorkflowListItem = ({ workflow }: { workflow: WorkflowRecordListIte
label={t('workflows.download')}
// This prevents an issue where the tooltip isn't closed after the modal is opened
isOpen={!isHovered ? false : undefined}
closeOnScroll
>
<IconButton
size="sm"
Expand All @@ -164,6 +166,7 @@ export const WorkflowListItem = ({ workflow }: { workflow: WorkflowRecordListIte
label={t('workflows.copyShareLink')}
// This prevents an issue where the tooltip isn't closed after the modal is opened
isOpen={!isHovered ? false : undefined}
closeOnScroll
>
<IconButton
size="sm"
Expand All @@ -179,6 +182,7 @@ export const WorkflowListItem = ({ workflow }: { workflow: WorkflowRecordListIte
label={t('workflows.delete')}
// This prevents an issue where the tooltip isn't closed after the modal is opened
isOpen={!isHovered ? false : undefined}
closeOnScroll
>
<IconButton
size="sm"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const FALLBACK_ICON_SIZE = '24px';
const StylePresetImage = ({ presetImageUrl, imageWidth }: { presetImageUrl: string | null; imageWidth?: number }) => {
return (
<Tooltip
closeOnScroll
label={
presetImageUrl && (
<Image
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles';
import { selectStylePresetSearchTerm } from 'features/stylePresets/store/stylePresetSlice';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
Expand All @@ -23,7 +24,7 @@ export const StylePresetList = ({ title, data }: { title: string; data: StylePre
</Text>
</Flex>
</Button>
<Collapse in={isOpen}>
<Collapse in={isOpen} style={fixTooltipCloseOnScrollStyles}>
{data.length ? (
data.map((preset) => <StylePresetListItem preset={preset} key={preset.id} />)
) : (
Expand Down
Loading