修复编辑器底栏选项面板悬停交互
将生成规范和生成音乐底栏入口改为悬停打开、离开收起 将生成音乐选项面板锚定到音乐按钮上方,避免错位 补充底栏 hover 行为和音乐菜单定位测试 更新生成类面板 Lovart 交互文档
This commit is contained in:
@@ -56,6 +56,8 @@
|
||||
- 父级面板、底部选项框、弹出子面板字号保持一致。
|
||||
- 底部组合值使用 `·` 分隔,例如 `16:9 · 4秒 · 480p`。
|
||||
- 底部参数热区与生成按钮等高,默认不显示阴影;悬停显示轻量阴影;箭头默认向下,展开后旋转向上。
|
||||
- 底边栏中会在上方弹出二级选项的入口不再依赖点击展开。鼠标悬停到入口即可打开二级面板,鼠标离开入口和二级面板后自动收起;当前范围包括 `生成规范` 和 `生成音乐`。
|
||||
- 底边栏二级选项面板必须锚定到对应入口按钮本身,不使用屏幕居中或固定底部偏移;移动端窄屏下也应保持跟随入口位置。
|
||||
- 生成图片和生成视频文本输入框紧贴参考图下方,取消旧网格预留导致的空白高度。
|
||||
- 不再在底部常驻展开全部可选项。
|
||||
|
||||
@@ -124,6 +126,8 @@
|
||||
- 多文本输入面板字段标题和边界仍清晰。
|
||||
- 生成按钮内能看到 `N泥点` 文案。
|
||||
- 弹出选项面板通过 `role=menu` 暴露,点击选项后保持打开,可连续修改多个字段。
|
||||
- 底边栏 `生成规范` / `生成音乐` 悬停即可看到上方选项面板;鼠标从按钮移动到选项面板时不闪退,离开按钮和选项面板后收起。
|
||||
- `生成音乐` 选项面板出现在音乐按钮上方,不再固定在底栏中间。
|
||||
- 规范面板比图片生成面板更紧凑,字段间距和输入高度更小,但外层 shell、首行参考图和底部按钮区必须继续对齐生成图片 / 生成角色 / 生成视频。
|
||||
- 生成视频结果以视频图层加入画布,画布媒体元素标记为 `画布视频:生成视频 N`。
|
||||
- 生成器输入、参数、参考图和占位框在刷新后仍存在;已生成对象的生成器面板继续跟随成品图层。
|
||||
|
||||
@@ -9,11 +9,16 @@ import { ImageCanvasBottomToolbarView } from './ImageCanvasBottomToolbarView';
|
||||
describe('ImageCanvasBottomToolbarView', () => {
|
||||
it('renders the canvas tools and forwards tool changes', () => {
|
||||
const switchTool = vi.fn();
|
||||
const specToolWrapRef = createRef<HTMLSpanElement>();
|
||||
const musicToolWrapRef = createRef<HTMLSpanElement>();
|
||||
render(
|
||||
<ImageCanvasBottomToolbarView
|
||||
specToolWrapRef={createRef<HTMLSpanElement>()}
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
musicToolWrapRef={musicToolWrapRef}
|
||||
effectiveTool="generate"
|
||||
onSwitchTool={switchTool}
|
||||
onOpenToolOptions={vi.fn()}
|
||||
onCloseToolOptions={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -48,13 +53,18 @@ describe('ImageCanvasBottomToolbarView', () => {
|
||||
});
|
||||
|
||||
it('only keeps the select and hand tools visibly pressed', () => {
|
||||
const specToolWrapRef = createRef<HTMLSpanElement>();
|
||||
const musicToolWrapRef = createRef<HTMLSpanElement>();
|
||||
const renderToolbar = (effectiveTool: Parameters<
|
||||
typeof ImageCanvasBottomToolbarView
|
||||
>[0]['effectiveTool']) => (
|
||||
<ImageCanvasBottomToolbarView
|
||||
specToolWrapRef={createRef<HTMLSpanElement>()}
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
musicToolWrapRef={musicToolWrapRef}
|
||||
effectiveTool={effectiveTool}
|
||||
onSwitchTool={vi.fn()}
|
||||
onOpenToolOptions={vi.fn()}
|
||||
onCloseToolOptions={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -98,4 +108,30 @@ describe('ImageCanvasBottomToolbarView', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('opens and closes bottom option tools on hover', () => {
|
||||
const openToolOptions = vi.fn();
|
||||
const closeToolOptions = vi.fn();
|
||||
const specToolWrapRef = createRef<HTMLSpanElement>();
|
||||
const musicToolWrapRef = createRef<HTMLSpanElement>();
|
||||
render(
|
||||
<ImageCanvasBottomToolbarView
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
musicToolWrapRef={musicToolWrapRef}
|
||||
effectiveTool="select"
|
||||
onSwitchTool={vi.fn()}
|
||||
onOpenToolOptions={openToolOptions}
|
||||
onCloseToolOptions={closeToolOptions}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.pointerEnter(specToolWrapRef.current!);
|
||||
fireEvent.pointerLeave(specToolWrapRef.current!);
|
||||
fireEvent.pointerEnter(musicToolWrapRef.current!);
|
||||
fireEvent.pointerLeave(musicToolWrapRef.current!);
|
||||
|
||||
expect(openToolOptions).toHaveBeenNthCalledWith(1, 'spec');
|
||||
expect(closeToolOptions).toHaveBeenNthCalledWith(1, 'spec');
|
||||
expect(openToolOptions).toHaveBeenNthCalledWith(2, 'music');
|
||||
expect(closeToolOptions).toHaveBeenNthCalledWith(2, 'music');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,10 +18,15 @@ import type { RefObject } from 'react';
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type { CanvasTool } from './ImageCanvasEditorTypes';
|
||||
|
||||
type ToolbarOptionTool = Extract<CanvasTool, 'music' | 'spec'>;
|
||||
|
||||
type ImageCanvasBottomToolbarViewProps = {
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
musicToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
effectiveTool: CanvasTool;
|
||||
onSwitchTool: (tool: CanvasTool) => void;
|
||||
onOpenToolOptions: (tool: ToolbarOptionTool) => void;
|
||||
onCloseToolOptions: (tool: ToolbarOptionTool) => void;
|
||||
};
|
||||
|
||||
const canvasTools: Array<{
|
||||
@@ -44,14 +49,21 @@ const canvasTools: Array<{
|
||||
{ id: 'export', label: '导出工具', icon: Download },
|
||||
];
|
||||
|
||||
function isToolbarOptionTool(tool: CanvasTool): tool is ToolbarOptionTool {
|
||||
return tool === 'music' || tool === 'spec';
|
||||
}
|
||||
|
||||
function hasPersistentPressedState(tool: CanvasTool) {
|
||||
return tool === 'select' || tool === 'hand';
|
||||
}
|
||||
|
||||
export function ImageCanvasBottomToolbarView({
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
effectiveTool,
|
||||
onSwitchTool,
|
||||
onOpenToolOptions,
|
||||
onCloseToolOptions,
|
||||
}: ImageCanvasBottomToolbarViewProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -60,22 +72,35 @@ export function ImageCanvasBottomToolbarView({
|
||||
aria-label="AI画布工具栏"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{canvasTools.map(({ id, label, icon: Icon }) =>
|
||||
id === 'spec' ? (
|
||||
<span
|
||||
key={id}
|
||||
ref={specToolWrapRef}
|
||||
className="image-canvas-editor__spec-tool-wrap"
|
||||
>
|
||||
<EditorIconButton
|
||||
label={label}
|
||||
title={label}
|
||||
icon={Icon}
|
||||
pressed={hasPersistentPressedState(id) && effectiveTool === id}
|
||||
onClick={() => onSwitchTool(id)}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
{canvasTools.map(({ id, label, icon: Icon }) => {
|
||||
if (isToolbarOptionTool(id)) {
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
ref={id === 'spec' ? specToolWrapRef : musicToolWrapRef}
|
||||
className={[
|
||||
'image-canvas-editor__bottom-toolbar-option-wrap',
|
||||
id === 'spec' ? 'image-canvas-editor__spec-tool-wrap' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onPointerEnter={() => onOpenToolOptions(id)}
|
||||
onPointerLeave={() => onCloseToolOptions(id)}
|
||||
onFocus={() => onOpenToolOptions(id)}
|
||||
onBlur={() => onCloseToolOptions(id)}
|
||||
>
|
||||
<EditorIconButton
|
||||
label={label}
|
||||
title={label}
|
||||
icon={Icon}
|
||||
pressed={hasPersistentPressedState(id) && effectiveTool === id}
|
||||
onClick={() => onSwitchTool(id)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorIconButton
|
||||
key={id}
|
||||
label={label}
|
||||
@@ -84,8 +109,8 @@ export function ImageCanvasBottomToolbarView({
|
||||
pressed={hasPersistentPressedState(id) && effectiveTool === id}
|
||||
onClick={() => onSwitchTool(id)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ function createStageProps(): ImageCanvasStageViewProps {
|
||||
return {
|
||||
canvasViewportRef: createRef<HTMLDivElement>(),
|
||||
specToolWrapRef: createRef<HTMLSpanElement>(),
|
||||
musicToolWrapRef: createRef<HTMLSpanElement>(),
|
||||
isPanning: false,
|
||||
effectiveTool: 'select',
|
||||
canvasBackgroundColor: '#f8fafc',
|
||||
@@ -191,6 +192,8 @@ function createStageProps(): ImageCanvasStageViewProps {
|
||||
onToggleMinimap: vi.fn(),
|
||||
onMinimapPointerDown: vi.fn(),
|
||||
onSwitchTool: vi.fn(),
|
||||
onOpenToolOptions: vi.fn(),
|
||||
onCloseToolOptions: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export function ImageCanvasEditorView() {
|
||||
const captureCanvasHistoryRef = useRef<() => void>(() => {});
|
||||
const resetCanvasInteractionStateRef = useRef<() => void>(() => {});
|
||||
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
||||
const musicToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
||||
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
@@ -444,6 +445,7 @@ export function ImageCanvasEditorView() {
|
||||
setViewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
generationReferenceButtonRef,
|
||||
@@ -782,6 +784,7 @@ export function ImageCanvasEditorView() {
|
||||
const stageProps = {
|
||||
canvasViewportRef,
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
isPanning,
|
||||
effectiveTool,
|
||||
canvasBackgroundColor,
|
||||
@@ -864,6 +867,8 @@ export function ImageCanvasEditorView() {
|
||||
onToggleMinimap: toggleMinimap,
|
||||
onMinimapPointerDown: handleMinimapPointerDown,
|
||||
onSwitchTool: switchTool,
|
||||
onOpenToolOptions: generationSurface.openToolbarOptionMenu,
|
||||
onCloseToolOptions: generationSurface.closeToolbarOptionMenu,
|
||||
children: generationComposerNode,
|
||||
};
|
||||
|
||||
|
||||
@@ -101,6 +101,8 @@ type ImageCanvasGenerationComposerViewProps = {
|
||||
onAddIconDescription: () => void;
|
||||
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
|
||||
onRememberImageModel: (model: string) => void;
|
||||
onSpecMenuPointerEnter?: () => void;
|
||||
onSpecMenuPointerLeave?: () => void;
|
||||
};
|
||||
|
||||
function buildPortalMenuStyle(
|
||||
@@ -864,6 +866,8 @@ export function ImageCanvasGenerationComposerView({
|
||||
onAddIconDescription,
|
||||
onUpdateCharacterAnimationDuration,
|
||||
onRememberImageModel,
|
||||
onSpecMenuPointerEnter,
|
||||
onSpecMenuPointerLeave,
|
||||
}: ImageCanvasGenerationComposerViewProps) {
|
||||
return (
|
||||
<>
|
||||
@@ -874,6 +878,10 @@ export function ImageCanvasGenerationComposerView({
|
||||
label="生成规范类型"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(specToolWrapRef.current, 'above')}
|
||||
onPointerEnter={onSpecMenuPointerEnter}
|
||||
onPointerLeave={onSpecMenuPointerLeave}
|
||||
onFocus={onSpecMenuPointerEnter}
|
||||
onBlur={onSpecMenuPointerLeave}
|
||||
>
|
||||
{(['character', 'ui', 'custom'] as const).map((specType) => (
|
||||
<PlatformFloatingMenuItem
|
||||
|
||||
@@ -31,6 +31,7 @@ import type {
|
||||
export type ImageCanvasStageViewProps = {
|
||||
canvasViewportRef: RefObject<HTMLDivElement | null>;
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
musicToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
isPanning: boolean;
|
||||
effectiveTool: CanvasTool;
|
||||
canvasBackgroundColor: string;
|
||||
@@ -120,11 +121,14 @@ export type ImageCanvasStageViewProps = {
|
||||
onToggleMinimap: () => void;
|
||||
onMinimapPointerDown: (event: ReactPointerEvent<HTMLButtonElement>) => void;
|
||||
onSwitchTool: (tool: CanvasTool) => void;
|
||||
onOpenToolOptions: (tool: Extract<CanvasTool, 'music' | 'spec'>) => void;
|
||||
onCloseToolOptions: (tool: Extract<CanvasTool, 'music' | 'spec'>) => void;
|
||||
};
|
||||
|
||||
export function ImageCanvasStageView({
|
||||
canvasViewportRef,
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
isPanning,
|
||||
effectiveTool,
|
||||
canvasBackgroundColor,
|
||||
@@ -202,6 +206,8 @@ export function ImageCanvasStageView({
|
||||
onToggleMinimap,
|
||||
onMinimapPointerDown,
|
||||
onSwitchTool,
|
||||
onOpenToolOptions,
|
||||
onCloseToolOptions,
|
||||
}: ImageCanvasStageViewProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -314,8 +320,11 @@ export function ImageCanvasStageView({
|
||||
|
||||
<ImageCanvasBottomToolbarView
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
musicToolWrapRef={musicToolWrapRef}
|
||||
effectiveTool={effectiveTool}
|
||||
onSwitchTool={onSwitchTool}
|
||||
onOpenToolOptions={onOpenToolOptions}
|
||||
onCloseToolOptions={onCloseToolOptions}
|
||||
/>
|
||||
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -60,6 +60,7 @@ function GenerationSurfaceHarness() {
|
||||
const [, setImageContextMenu] = useState<ImageContextMenuState | null>(null);
|
||||
const layerCounterRef = useRef(0);
|
||||
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
||||
const musicToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
||||
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
@@ -78,6 +79,7 @@ function GenerationSurfaceHarness() {
|
||||
setViewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
@@ -105,6 +107,13 @@ function GenerationSurfaceHarness() {
|
||||
return (
|
||||
<div>
|
||||
<span ref={specToolWrapRef}>规范入口</span>
|
||||
<span
|
||||
ref={musicToolWrapRef}
|
||||
onPointerEnter={() => surface.openToolbarOptionMenu('music')}
|
||||
onPointerLeave={() => surface.closeToolbarOptionMenu('music')}
|
||||
>
|
||||
音乐入口
|
||||
</span>
|
||||
<span data-testid="tool">{activeTool}</span>
|
||||
<span data-testid="sidebar">{activeSidebarPanel ?? '-'}</span>
|
||||
<span data-testid="dialog">
|
||||
@@ -195,6 +204,51 @@ describe('useImageCanvasGenerationSurface', () => {
|
||||
expect(within(menu).getByRole('menuitem', { name: '角色形象规范' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('anchors the music menu to the music tool and closes after hover leaves', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
render(<GenerationSurfaceHarness />);
|
||||
const musicAnchor = screen.getByText('音乐入口');
|
||||
Object.defineProperty(musicAnchor, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => ({
|
||||
x: 220,
|
||||
y: 360,
|
||||
left: 220,
|
||||
top: 360,
|
||||
right: 256,
|
||||
bottom: 396,
|
||||
width: 36,
|
||||
height: 36,
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
});
|
||||
|
||||
fireEvent.pointerEnter(musicAnchor);
|
||||
|
||||
const menu = screen.getByRole('menu', { name: '生成音乐类型' });
|
||||
expect(menu.style.left).toBe('220px');
|
||||
expect(menu.style.top).toBe('360px');
|
||||
expect(menu.style.transform).toBe('translateY(calc(-100% - 0.45rem))');
|
||||
expect(menu.getAttribute('style') ?? '').not.toContain('left: 50%');
|
||||
|
||||
fireEvent.pointerLeave(musicAnchor);
|
||||
fireEvent.pointerEnter(menu);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200);
|
||||
});
|
||||
expect(screen.getByRole('menu', { name: '生成音乐类型' })).toBeTruthy();
|
||||
|
||||
fireEvent.pointerLeave(menu);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200);
|
||||
});
|
||||
expect(screen.queryByRole('menu', { name: '生成音乐类型' })).toBeNull();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('opens video and UI design generation dialogs through the generation surface', () => {
|
||||
render(<GenerationSurfaceHarness />);
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Dispatch,
|
||||
type MutableRefObject,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
@@ -36,6 +39,8 @@ type CanvasGenerationDialogUpdater = (
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => CanvasGenerationDialogState | null;
|
||||
|
||||
type ToolbarOptionTool = Extract<CanvasTool, 'music' | 'spec'>;
|
||||
|
||||
type ImageCanvasGenerationSurfaceOptions = {
|
||||
layers: CanvasLayer[];
|
||||
canvasSize: { width: number; height: number };
|
||||
@@ -43,6 +48,7 @@ type ImageCanvasGenerationSurfaceOptions = {
|
||||
setViewport: Dispatch<SetStateAction<CanvasViewport>>;
|
||||
layerCounterRef: MutableRefObject<number>;
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
musicToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
@@ -80,6 +86,34 @@ function renderEditorPortal(node: ReactNode) {
|
||||
return createPortal(node, document.body);
|
||||
}
|
||||
|
||||
function buildToolbarPortalMenuStyle(
|
||||
anchor: HTMLElement | null,
|
||||
): CSSProperties {
|
||||
const rect = anchor?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
zIndex: 70,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
position: 'fixed',
|
||||
left: Math.round(rect.left),
|
||||
top: Math.round(rect.top),
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
zIndex: 70,
|
||||
transform: 'translateY(calc(-100% - 0.45rem))',
|
||||
};
|
||||
}
|
||||
|
||||
const TOOLBAR_OPTION_CLOSE_DELAY_MS = 160;
|
||||
|
||||
export function useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize,
|
||||
@@ -87,6 +121,7 @@ export function useImageCanvasGenerationSurface({
|
||||
setViewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
musicToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
@@ -109,6 +144,9 @@ export function useImageCanvasGenerationSurface({
|
||||
requestUpload,
|
||||
persistGeneratedAsset,
|
||||
}: ImageCanvasGenerationSurfaceOptions) {
|
||||
const toolbarOptionCloseTimerRef = useRef<ReturnType<
|
||||
typeof setTimeout
|
||||
> | null>(null);
|
||||
const generationWorkflow = useImageCanvasGenerationWorkflow({
|
||||
layers,
|
||||
canvasSize,
|
||||
@@ -165,6 +203,44 @@ export function useImageCanvasGenerationSurface({
|
||||
canvasSize,
|
||||
});
|
||||
|
||||
const clearToolbarOptionCloseTimer = useCallback(() => {
|
||||
if (!toolbarOptionCloseTimerRef.current) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(toolbarOptionCloseTimerRef.current);
|
||||
toolbarOptionCloseTimerRef.current = null;
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
clearToolbarOptionCloseTimer();
|
||||
},
|
||||
[clearToolbarOptionCloseTimer],
|
||||
);
|
||||
|
||||
const openToolbarOptionMenu = useCallback(
|
||||
(tool: ToolbarOptionTool) => {
|
||||
clearToolbarOptionCloseTimer();
|
||||
generationWorkflow.setIsMusicMenuOpen(tool === 'music');
|
||||
generationWorkflow.setIsSpecMenuOpen(tool === 'spec');
|
||||
},
|
||||
[clearToolbarOptionCloseTimer, generationWorkflow],
|
||||
);
|
||||
|
||||
const closeToolbarOptionMenu = useCallback(
|
||||
(tool: ToolbarOptionTool) => {
|
||||
clearToolbarOptionCloseTimer();
|
||||
toolbarOptionCloseTimerRef.current = setTimeout(() => {
|
||||
if (tool === 'music') {
|
||||
generationWorkflow.setIsMusicMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
generationWorkflow.setIsSpecMenuOpen(false);
|
||||
}, TOOLBAR_OPTION_CLOSE_DELAY_MS);
|
||||
},
|
||||
[clearToolbarOptionCloseTimer, generationWorkflow],
|
||||
);
|
||||
|
||||
const switchGenerationTool = useCallback(
|
||||
(tool: CanvasTool) => {
|
||||
if (tool === 'generate') {
|
||||
@@ -176,12 +252,12 @@ export function useImageCanvasGenerationSurface({
|
||||
return true;
|
||||
}
|
||||
if (tool === 'music') {
|
||||
generationWorkflow.setIsMusicMenuOpen((open) => !open);
|
||||
openToolbarOptionMenu('music');
|
||||
setActiveTool('music');
|
||||
return true;
|
||||
}
|
||||
if (tool === 'spec') {
|
||||
generationWorkflow.setIsSpecMenuOpen((open) => !open);
|
||||
openToolbarOptionMenu('spec');
|
||||
setActiveTool('spec');
|
||||
return true;
|
||||
}
|
||||
@@ -199,7 +275,7 @@ export function useImageCanvasGenerationSurface({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[generationWorkflow, setActiveTool],
|
||||
[generationWorkflow, openToolbarOptionMenu, setActiveTool],
|
||||
);
|
||||
|
||||
const generationComposerNode = (
|
||||
@@ -302,6 +378,8 @@ export function useImageCanvasGenerationSurface({
|
||||
generationWorkflow.updateCharacterAnimationDuration
|
||||
}
|
||||
onRememberImageModel={generationWorkflow.rememberImageModel}
|
||||
onSpecMenuPointerEnter={() => openToolbarOptionMenu('spec')}
|
||||
onSpecMenuPointerLeave={() => closeToolbarOptionMenu('spec')}
|
||||
/>
|
||||
{generationWorkflow.isMusicMenuOpen
|
||||
? renderEditorPortal(
|
||||
@@ -309,13 +387,11 @@ export function useImageCanvasGenerationSurface({
|
||||
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu image-canvas-editor__music-menu"
|
||||
label="生成音乐类型"
|
||||
placement="top-start"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
bottom: '4.8rem',
|
||||
zIndex: 70,
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
style={buildToolbarPortalMenuStyle(musicToolWrapRef.current)}
|
||||
onPointerEnter={() => openToolbarOptionMenu('music')}
|
||||
onPointerLeave={() => closeToolbarOptionMenu('music')}
|
||||
onFocus={() => openToolbarOptionMenu('music')}
|
||||
onBlur={() => closeToolbarOptionMenu('music')}
|
||||
>
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={generationWorkflow.openSoundEffectGenerationDialog}
|
||||
@@ -337,6 +413,8 @@ export function useImageCanvasGenerationSurface({
|
||||
...generationWorkflow,
|
||||
generationComposerStyle,
|
||||
generationComposerNode,
|
||||
openToolbarOptionMenu,
|
||||
closeToolbarOptionMenu,
|
||||
switchGenerationTool,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4775,6 +4775,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.image-canvas-editor__bottom-toolbar-option-wrap,
|
||||
.image-canvas-editor__spec-tool-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
Reference in New Issue
Block a user