修复 UI 编辑器字体样张与资源选择

复用字体导入器和检查器的样张组件并固定各行字号

切换界面图、节点或精灵时清除字体资源选择

补充资源选择互斥回归测试
This commit is contained in:
2026-08-18 20:16:11 +08:00
parent 892c515d09
commit 37f7f710f3
6 changed files with 127 additions and 23 deletions
@@ -1,6 +1,7 @@
import { invoke } from '@tauri-apps/api/core';
import { useEffect, useRef, useState } from 'react';
import { FontSamplePreview } from '../../features/ui-editor/components/FontSamplePreview';
import type { FontAsset } from '../../features/ui-editor/types/FontAsset';
import {
type LoadedUiEditorFontFace,
@@ -13,6 +14,7 @@ export function FontImporterPreview({
selected,
}: AssetImporterPreviewProps) {
const [fontFamily, setFontFamily] = useState<string | null>(null);
const [font, setFont] = useState<FontAsset | null>(null);
const [error, setError] = useState<string | null>(null);
const loadedRef = useRef<{ face: FontFace; url: string } | null>(null);
@@ -25,6 +27,7 @@ export function FontImporterPreview({
loadedRef.current = null;
}
setFontFamily(null);
setFont(null);
setError(null);
if (!selected?.asset || selected.isDirectory) return;
void (async () => {
@@ -41,6 +44,7 @@ export function FontImporterPreview({
}
document.fonts.add(loaded.face);
loadedRef.current = loaded;
setFont(font);
setFontFamily(loaded.cssFamily);
} catch (cause) {
console.error('[ui-editor-font-preview] failed', {
@@ -69,17 +73,10 @@ export function FontImporterPreview({
);
}
return (
<div className="w-full px-5 py-6" style={{ color: '#000', fontFamily }}>
<p className="m-0 text-xs leading-5">
· English Font Preview · 0123456789
</p>
<p className="m-0 mt-3 text-sm leading-6">
Aa Bb Cc!? @#&amp;*()
</p>
<p className="m-0 mt-3 text-lg leading-7"> Genarrative Studio</p>
<p className="m-0 mt-3 text-2xl leading-8">Hello World! </p>
<p className="m-0 mt-3 text-3xl leading-10"> Aa 123</p>
<p className="m-0 mt-3 text-4xl leading-none"> English</p>
</div>
<FontSamplePreview
fontFamily={fontFamily}
fontWeight={font?.metadata.weight}
italic={font?.metadata.italic}
/>
);
}
@@ -0,0 +1,72 @@
export function FontSamplePreview({
fontFamily,
fontWeight,
italic,
}: {
fontFamily?: string;
fontWeight?: number;
italic?: boolean;
}) {
// 将字体样张的排版属性放到每一行上,而不是依赖 Inspector 容器的继承。
// Inspector 同时承载节点文本样式编辑,样张必须始终只反映当前字体资源。
const sampleStyle = fontFamily
? {
color: '#000',
fontFamily: JSON.stringify(fontFamily),
fontWeight,
fontStyle: italic ? 'italic' : 'normal',
fontSynthesis: 'none',
}
: { color: '#000' };
const sampleLines = [
{
text: '中文字体预览 · English Font Preview · 0123456789',
fontSize: 12,
lineHeight: '20px',
},
{
text: '春夏秋冬,山川湖海;Aa Bb Cc!? @#&*()',
fontSize: 14,
lineHeight: '24px',
},
{
text: '陶泥儿 Genarrative Studio',
fontSize: 18,
lineHeight: '28px',
},
{
text: '你好,Hello World! 字体设计',
fontSize: 24,
lineHeight: '32px',
},
{
text: '字体样张 Aa 字母数字 123',
fontSize: 30,
lineHeight: '40px',
},
{
text: '中文 English',
fontSize: 36,
lineHeight: '40px',
},
];
return (
<div className="w-full px-5 py-6">
{sampleLines.map((line, index) => (
<p
key={line.text}
className={index === 0 ? 'm-0' : 'm-0 mt-3'}
style={{
...sampleStyle,
fontSize: line.fontSize,
lineHeight: line.lineHeight,
}}
>
{line.text}
</p>
))}
</div>
);
}
@@ -6,6 +6,7 @@ import type {
TextareaHTMLAttributes,
} from 'react';
import { FontSamplePreview } from '../../../../features/ui-editor/components/FontSamplePreview';
import { SpriteImagePreview } from '../../../../features/ui-editor/components/SpriteImagePreview';
import type { UIDesignImageId } from '../../../../features/ui-editor/types/UIDesignImageId';
import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UIDesignImageRole';
@@ -593,19 +594,16 @@ function FontInspector({
return (
<div className="mt-4 space-y-3">
<div className="rounded-xl border border-(--platform-subpanel-border) bg-white/45 p-4">
<span
className="block truncate text-3xl"
style={
<div className="overflow-hidden rounded-xl border border-(--platform-subpanel-border) bg-white/45">
<FontSamplePreview
fontFamily={
loadStatus === 'loaded'
? {
fontFamily: JSON.stringify(uiEditorPrivateFontFamily(fontId)),
}
? uiEditorPrivateFontFamily(fontId)
: undefined
}
>
Aa 123
</span>
fontWeight={font.metadata.weight}
italic={font.metadata.italic}
/>
</div>
<InspectorReadout label="字体家族" value={font.metadata.family_name} />
<InspectorReadout label="字体面" value={font.metadata.face_name} />
@@ -7,10 +7,12 @@ import type { UiEditorNodeFocusRequest } from './model';
export function useUiEditorNodeFocus({
activateImage,
clearSpriteSelection,
clearFontSelection,
selectNode,
}: {
activateImage: (id: UIDesignImageId) => void;
clearSpriteSelection: () => void;
clearFontSelection: () => void;
selectNode: (id: NodeId) => void;
}) {
const nextRequestIdRef = useRef(0);
@@ -21,6 +23,7 @@ export function useUiEditorNodeFocus({
(treeId: UIDesignImageId, nodeId: NodeId) => {
activateImage(treeId);
clearSpriteSelection();
clearFontSelection();
selectNode(nodeId);
nextRequestIdRef.current += 1;
setFocusRequest({
@@ -29,7 +32,7 @@ export function useUiEditorNodeFocus({
requestId: nextRequestIdRef.current,
});
},
[activateImage, clearSpriteSelection, selectNode],
[activateImage, clearFontSelection, clearSpriteSelection, selectNode],
);
const clearFocusRequest = useCallback(() => setFocusRequest(null), []);
@@ -137,6 +137,7 @@ export function useUiEditorPage(
const { focusRequest, focusNode } = useUiEditorNodeFocus({
activateImage: setActiveImageId,
clearSpriteSelection: () => setSelectedSpriteId(null),
clearFontSelection: () => setSelectedFontId(null),
selectNode: setSelectedNodeId,
});
const [keepChildrenUnchanged, setKeepChildrenUnchanged] = useState(false);
@@ -607,6 +608,7 @@ export function useUiEditorPage(
setImageOrder([]);
setActiveImageId(null);
setSelectedSpriteId(null);
setSelectedFontId(null);
setSelectedNodeId(null);
setHiddenNodeIds(new Set());
setPreviewUrls({});
@@ -616,11 +618,13 @@ export function useUiEditorPage(
function selectDesignImage(id: UIDesignImageId) {
setActiveImageId(id);
setSelectedSpriteId(null);
setSelectedFontId(null);
setSelectedNodeId(null);
}
function selectNode(id: NodeId) {
setSelectedSpriteId(null);
setSelectedFontId(null);
setSelectedNodeId(id);
}
@@ -758,6 +762,7 @@ export function useUiEditorPage(
function selectSprite(id: SpriteAssetId) {
setSelectedNodeId(null);
setSelectedSpriteId(id);
setSelectedFontId(null);
}
function selectFont(id: FontAssetId) {
@@ -145,6 +145,21 @@ describe('UiEditorPage', () => {
border: { left: 0, right: 0, top: 0, bottom: 0 },
},
]);
result.current.editor.addFontAssets([
{
asset_id: 'body',
metadata: {
family_name: '测试字体',
face_name: 'Regular',
weight: 400,
italic: false,
format: 'TrueType',
source_file_name: 'body.ttf',
},
path: 'assets/fonts/body.ttf',
content_sha256: 'a'.repeat(64),
},
]);
});
const rootId = result.current.editor.state.ui_trees[0]!.root.id;
act(() => {
@@ -156,6 +171,20 @@ describe('UiEditorPage', () => {
act(() => result.current.selectSprite('panel'));
expect(result.current.selectedNodeId).toBeNull();
expect(result.current.selectedSpriteId).toBe('panel');
act(() => result.current.selectFont('body'));
expect(result.current.selectedFontId).toBe('body');
act(() => result.current.selectDesignImage('page'));
expect(result.current.selectedFontId).toBeNull();
act(() => result.current.selectFont('body'));
act(() => result.current.selectSprite('panel'));
expect(result.current.selectedFontId).toBeNull();
act(() => result.current.selectFont('body'));
act(() => result.current.selectNode(rootId));
expect(result.current.selectedFontId).toBeNull();
});
it('keeps node preview visibility transient and attached to global node IDs', () => {