|
43 | 43 |
|
44 | 44 | import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; |
45 | 45 | import { useNavigate } from "react-router-dom"; |
46 | | -import { useQueries } from "@tanstack/react-query"; |
47 | 46 | import { type VirtualItem, useVirtualizer } from "@tanstack/react-virtual"; |
48 | 47 | import { CELL_HEIGHT, GlyphPreview } from "@/components/home/GlyphPreview"; |
49 | | -import { useEditor, useWorkspace } from "@/workspace/WorkspaceContext"; |
| 48 | +import { useEditor } from "@/workspace/WorkspaceContext"; |
50 | 49 | import { getGlyphInfo } from "@/workspace/glyphInfo"; |
51 | 50 | import { type GlyphCatalogItem, useGlyphCatalog } from "@/context/GlyphCatalogContext"; |
52 | | -import { useSignalState } from "@/lib/signals"; |
| 51 | +import { useGlyphViews } from "@/hooks/useGlyphViews"; |
53 | 52 | import { Button, Input } from "@shift/ui"; |
54 | | -import type { GlyphId, GlyphName, GlyphPreview as GlyphPreviewValue } from "@shift/types"; |
| 53 | +import type { GlyphId, GlyphName } from "@shift/types"; |
55 | 54 |
|
56 | 55 | const ROW_HEIGHT = CELL_HEIGHT + 40 + 8; |
57 | 56 | const NOMINAL_CELL_WIDTH = 100; |
58 | 57 | const GAP = 8; |
59 | 58 | const SCROLL_PADDING = 4; |
60 | 59 | const ROW_PADDING_X = 4; |
61 | 60 | const OVERSCAN = 5; |
62 | | -const GLYPH_QUERY_CHUNK_SIZE = 32; |
63 | | -const GLYPH_QUERY_OVERSCAN_CHUNKS = 1; |
64 | | -const GLYPH_QUERY_GC_TIME = 30_000; |
| 61 | +const PROJECTION_OVERSCAN = 32; |
65 | 62 |
|
66 | 63 | function computeLayout(width: number) { |
67 | 64 | const availableWidth = width - SCROLL_PADDING - ROW_PADDING_X; |
@@ -91,13 +88,9 @@ function visibleGlyphRowsForRows( |
91 | 88 |
|
92 | 89 | export const GlyphGrid = memo(function GlyphGrid() { |
93 | 90 | const navigate = useNavigate(); |
94 | | - const workspace = useWorkspace(); |
95 | | - const editor = workspace.editor; |
| 91 | + const editor = useEditor(); |
96 | 92 | const font = editor.font; |
97 | 93 | const metrics = font.metrics; |
98 | | - const designLocation = useSignalState(editor.designLocationCell, { schedule: "frame" }); |
99 | | - const documentState = useSignalState(workspace.documentStateCell); |
100 | | - const documentId = documentState?.documentId ?? null; |
101 | 94 | const { filteredGlyphs: catalogGlyphs } = useGlyphCatalog(); |
102 | 95 |
|
103 | 96 | const scrollContainerRef = useRef<HTMLDivElement>(null); |
@@ -148,59 +141,17 @@ export const GlyphGrid = memo(function GlyphGrid() { |
148 | 141 | const visibleEndIndex = lastVisibleRow |
149 | 142 | ? Math.min(catalogGlyphs.length, (lastVisibleRow.index + 1) * columns) |
150 | 143 | : 0; |
151 | | - const previewGlyphChunks = useMemo((): readonly (readonly GlyphId[])[] => { |
| 144 | + const previewGlyphIds = useMemo((): readonly GlyphId[] => { |
152 | 145 | if (visibleEndIndex <= visibleStartIndex) return []; |
153 | 146 |
|
154 | | - const queryStartIndex = Math.max( |
155 | | - 0, |
156 | | - visibleStartIndex - GLYPH_QUERY_OVERSCAN_CHUNKS * GLYPH_QUERY_CHUNK_SIZE, |
157 | | - ); |
158 | | - const queryEndIndex = Math.min( |
159 | | - catalogGlyphs.length, |
160 | | - visibleEndIndex + GLYPH_QUERY_OVERSCAN_CHUNKS * GLYPH_QUERY_CHUNK_SIZE, |
161 | | - ); |
162 | | - const firstChunkStart = |
163 | | - Math.floor(queryStartIndex / GLYPH_QUERY_CHUNK_SIZE) * GLYPH_QUERY_CHUNK_SIZE; |
164 | | - const chunks: GlyphId[][] = []; |
165 | | - |
166 | | - for ( |
167 | | - let chunkStart = firstChunkStart; |
168 | | - chunkStart < queryEndIndex; |
169 | | - chunkStart += GLYPH_QUERY_CHUNK_SIZE |
170 | | - ) { |
171 | | - chunks.push( |
172 | | - catalogGlyphs |
173 | | - .slice(chunkStart, chunkStart + GLYPH_QUERY_CHUNK_SIZE) |
174 | | - .map((glyph) => glyph.id), |
175 | | - ); |
176 | | - } |
177 | | - |
178 | | - return chunks; |
| 147 | + const startIndex = Math.max(0, visibleStartIndex - PROJECTION_OVERSCAN); |
| 148 | + const endIndex = Math.min(catalogGlyphs.length, visibleEndIndex + PROJECTION_OVERSCAN); |
| 149 | + return catalogGlyphs.slice(startIndex, endIndex).map((glyph) => glyph.id); |
179 | 150 | }, [catalogGlyphs, visibleEndIndex, visibleStartIndex]); |
180 | | - const locationKey = useMemo( |
181 | | - () => [...designLocation].sort(([left], [right]) => left.localeCompare(right)), |
182 | | - [designLocation], |
183 | | - ); |
184 | | - const glyphChunkQueries = useQueries({ |
185 | | - queries: previewGlyphChunks.map((glyphIds) => ({ |
186 | | - queryKey: ["glyph-previews", documentId, locationKey, glyphIds] as const, |
187 | | - queryFn: async (): Promise<readonly GlyphPreviewValue[]> => { |
188 | | - try { |
189 | | - return await font.glyphPreviews(glyphIds, designLocation); |
190 | | - } catch (error) { |
191 | | - console.error("failed to resolve glyph preview chunk", error); |
192 | | - throw error; |
193 | | - } |
194 | | - }, |
195 | | - enabled: documentId !== null, |
196 | | - staleTime: Infinity, |
197 | | - gcTime: GLYPH_QUERY_GC_TIME, |
198 | | - })), |
199 | | - }); |
200 | | - const previewsByGlyphId = new Map( |
201 | | - glyphChunkQueries |
202 | | - .flatMap((query) => query.data ?? []) |
203 | | - .map((preview) => [preview.glyphId, preview] as const), |
| 151 | + const glyphViews = useGlyphViews(font, previewGlyphIds, editor.designLocationCell); |
| 152 | + const viewsByGlyphId = useMemo( |
| 153 | + () => new Map(previewGlyphIds.map((glyphId, index) => [glyphId, glyphViews[index]] as const)), |
| 154 | + [glyphViews, previewGlyphIds], |
204 | 155 | ); |
205 | 156 |
|
206 | 157 | const handleCellClick = useCallback( |
@@ -263,8 +214,7 @@ export const GlyphGrid = memo(function GlyphGrid() { |
263 | 214 | onClick={() => handleCellClick(glyph)} |
264 | 215 | > |
265 | 216 | <GlyphPreview |
266 | | - preview={previewsByGlyphId.get(glyph.id) ?? null} |
267 | | - unicode={glyph.unicode} |
| 217 | + view={viewsByGlyphId.get(glyph.id) ?? null} |
268 | 218 | metrics={metrics} |
269 | 219 | height={CELL_HEIGHT} |
270 | 220 | /> |
|
0 commit comments