修复 UI 编辑器字体样张与资源选择
复用字体导入器和检查器的样张组件并固定各行字号 切换界面图、节点或精灵时清除字体资源选择 补充资源选择互斥回归测试
This commit is contained in:
@@ -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,!? @#&*()
|
||||
</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>
|
||||
);
|
||||
}
|
||||
+8
-10
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user