| import type { SystemStyleObject } from '@invoke-ai/ui-library'; | |
| import { | |
| Box, | |
| Button, | |
| chakra, | |
| Flex, | |
| Icon, | |
| Popover, | |
| PopoverArrow, | |
| PopoverBody, | |
| PopoverContent, | |
| PopoverTrigger, | |
| Portal, | |
| Text, | |
| useCheckbox, | |
| useToken, | |
| } from '@invoke-ai/ui-library'; | |
| import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; | |
| import { selectSendToCanvas, settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice'; | |
| import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; | |
| import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; | |
| import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice'; | |
| import type { ChangeEvent, PropsWithChildren } from 'react'; | |
| import { memo, useCallback, useMemo } from 'react'; | |
| import { Trans, useTranslation } from 'react-i18next'; | |
| import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi'; | |
| const getSx = (padding: string | number): SystemStyleObject => ({ | |
| bg: 'base.700', | |
| w: '72px', | |
| cursor: 'pointer', | |
| '&[data-checked]': { | |
| '.thumb': { | |
| left: `calc(100% - ${padding})`, | |
| transform: 'translateX(-100%)', | |
| bg: 'invokeGreen.300', | |
| }, | |
| '.unchecked-icon': { | |
| color: 'base.50', | |
| opacity: 0.4, | |
| }, | |
| '.checked-icon': { | |
| color: 'base.900', | |
| opacity: 1, | |
| }, | |
| }, | |
| '&[data-disabled]': { | |
| bg: 'base.700', | |
| '.thumb': { | |
| bg: 'base.500', | |
| }, | |
| '.unchecked-icon': { | |
| color: 'base.800', | |
| }, | |
| '.checked-icon': { | |
| color: 'base.800', | |
| }, | |
| }, | |
| '.thumb': { | |
| transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out', | |
| left: padding, | |
| transform: 'translateX(0)', | |
| bg: 'invokeBlue.400', | |
| shadow: 'md', | |
| }, | |
| '.unchecked-icon': { | |
| color: 'base.900', | |
| opacity: 1, | |
| }, | |
| '.checked-icon': { | |
| color: 'base.50', | |
| opacity: 0.4, | |
| }, | |
| }); | |
| export const SendToToggle = memo(() => { | |
| const { t } = useTranslation(); | |
| const dispatch = useAppDispatch(); | |
| const sendToCanvas = useAppSelector(selectSendToCanvas); | |
| const isStaging = useAppSelector(selectIsStaging); | |
| const gap = useToken('space', 1); | |
| const sx = useMemo(() => getSx(gap), [gap]); | |
| const onChange = useCallback( | |
| (e: ChangeEvent<HTMLInputElement>) => { | |
| dispatch(settingsSendToCanvasChanged(e.target.checked)); | |
| }, | |
| [dispatch] | |
| ); | |
| const { getCheckboxProps, getInputProps, htmlProps } = useCheckbox({ | |
| onChange, | |
| isChecked: sendToCanvas, | |
| isDisabled: isStaging, | |
| }); | |
| return ( | |
| <Popover trigger="hover"> | |
| <PopoverTrigger> | |
| <chakra.label {...htmlProps}> | |
| <Flex | |
| position="relative" | |
| borderRadius="base" | |
| alignItems="center" | |
| justifyContent="space-between" | |
| h="full" | |
| p={gap} | |
| gap={gap} | |
| sx={sx} | |
| {...getCheckboxProps()} | |
| > | |
| <input {...getInputProps()} hidden /> | |
| <Box className="thumb" position="absolute" borderRadius="base" w={10} top={gap} bottom={gap} /> | |
| <Flex w={10} h="full" alignItems="center" justifyContent="center" pos="relative"> | |
| <Icon | |
| className="unchecked-icon" | |
| w={6} | |
| h={6} | |
| as={PiImageBold} | |
| aria-label={t('controlLayers.sendToGallery')} | |
| /> | |
| </Flex> | |
| <Flex w={10} h="full" alignItems="center" justifyContent="center" pos="relative"> | |
| <Icon | |
| className="checked-icon" | |
| w={6} | |
| h={6} | |
| as={PiPaintBrushBold} | |
| aria-label={t('controlLayers.sendToCanvas')} | |
| /> | |
| </Flex> | |
| </Flex> | |
| </chakra.label> | |
| </PopoverTrigger> | |
| <Portal> | |
| <PopoverContent maxW={296} p={2} bg="base.200" color="base.800"> | |
| <PopoverArrow | |
| bg="base.200" | |
| left={sendToCanvas ? '18px !important' : '-18px !important'} | |
| transitionProperty="all" | |
| transitionDuration="0.2s" | |
| /> | |
| <PopoverBody> | |
| <TooltipContent /> | |
| </PopoverBody> | |
| </PopoverContent> | |
| </Portal> | |
| </Popover> | |
| ); | |
| }); | |
| SendToToggle.displayName = 'SendToToggle'; | |
| const TooltipContent = memo(() => { | |
| const { t } = useTranslation(); | |
| const sendToCanvas = useAppSelector(selectSendToCanvas); | |
| const isStaging = useAppSelector(selectIsStaging); | |
| if (isStaging) { | |
| return ( | |
| <Flex flexDir="column"> | |
| <Text fontWeight="semibold">{t('controlLayers.sendingToCanvas')}</Text> | |
| <Text fontWeight="normal"> | |
| <Trans i18nKey="controlLayers.viewProgressOnCanvas" components={{ Btn: <ActivateCanvasButton /> }} /> | |
| </Text> | |
| </Flex> | |
| ); | |
| } | |
| return ( | |
| <Flex flexDir="column"> | |
| <Text fontWeight="semibold"> | |
| {sendToCanvas ? t('controlLayers.sendToCanvas') : t('controlLayers.sendToGallery')} | |
| </Text> | |
| <Text fontWeight="normal"> | |
| {sendToCanvas ? t('controlLayers.sendToCanvasDesc') : t('controlLayers.sendToGalleryDesc')} | |
| </Text> | |
| </Flex> | |
| ); | |
| }); | |
| TooltipContent.displayName = 'TooltipContent'; | |
| const ActivateCanvasButton = memo((props: PropsWithChildren) => { | |
| const dispatch = useAppDispatch(); | |
| const onClick = useCallback(() => { | |
| dispatch(setActiveTab('canvas')); | |
| dispatch(activeTabCanvasRightPanelChanged('layers')); | |
| $imageViewer.set(false); | |
| }, [dispatch]); | |
| return ( | |
| <Button | |
| onClick={onClick} | |
| size="sm" | |
| variant="link" | |
| color="base.800" | |
| _hover={{ color: 'base.900', textDecoration: 'underline' }} | |
| > | |
| {props.children} | |
| </Button> | |
| ); | |
| }); | |
| ActivateCanvasButton.displayName = 'ActivateCanvasButton'; | |