Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 1 addition & 24 deletions apps/desktop/src/renderer/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { ZoomToast } from "@/components/chrome/ZoomToast";
import { isDev } from "@/lib/utils/utils";
import { dumpSelectionPatternsToConsole } from "@/lib/debug/dumpSelectionPatterns";
import { getDocument } from "@/store/store";
import { documentPersistence } from "@/persistence";

import { RouteDispatcher } from "./RouteDispatcher";

Expand Down Expand Up @@ -41,14 +40,8 @@ export const App = () => {
useEffect(() => {
const fontDocument = getDocument();
const editor = fontDocument.editor;
documentPersistence.init(editor);
let didOpenFont = false;

const handleBeforeUnload = () => {
documentPersistence.flushNow();
};
window.addEventListener("beforeunload", handleBeforeUnload);

const handleOpenFont = (filePath: string, source: "event" | "restore" = "event") => {
if (source === "restore" && didOpenFont) {
return;
Expand Down Expand Up @@ -79,21 +72,7 @@ export const App = () => {
return;
}

const state = documentPersistence.getState();
const mostRecentDocId = state.registry.lruDocIds[0];
const mostRecentPath = mostRecentDocId ? state.registry.docIdToPath[mostRecentDocId] : null;
if (!mostRecentPath) {
fontDocument.createFont();
return;
}

const exists = await window.electronAPI?.pathsExist([mostRecentPath]);
if (!exists?.[0]) {
fontDocument.createFont();
return;
}

handleOpenFont(mostRecentPath, "restore");
fontDocument.createFont();
})();
} else if (needsLoadedDocument(window.location.hash) && !fontDocument.loaded) {
fontDocument.createFont();
Expand Down Expand Up @@ -128,8 +107,6 @@ export const App = () => {
});

return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
documentPersistence.dispose();
if (unsubscribeNew) unsubscribeNew();
if (unsubscribeOpen) unsubscribeOpen();
if (unsubscribeExternalOpen) unsubscribeExternalOpen();
Expand Down
35 changes: 1 addition & 34 deletions apps/desktop/src/renderer/src/app/Document.test.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,13 @@
import { describe, expect, it } from "vitest";
import { TestEditor } from "@/testing/TestEditor";
import { MUTATORSANS_DESIGNSPACE } from "@/testing/fixtures";
import { Document, type DocumentPersistence } from "./Document";

class InMemoryDocumentPersistence implements DocumentPersistence {
currentDocId: string | null = null;
currentPath: string | null = null;

closeDocument(): void {
this.currentDocId = null;
this.currentPath = null;
}

openDocument(filePath: string): void {
this.currentPath = filePath;
this.currentDocId = `file:${filePath}`;
}

openUntitledDocument(docId: string): void {
this.currentDocId = docId;
this.currentPath = null;
}

onDocumentPathChanged(filePath: string | null): void {
this.currentPath = filePath;
if (filePath) this.currentDocId ??= `file:${filePath}`;
}

flushNow(): void {}
}
import { Document } from "./Document";

function testDocument() {
const editor = new TestEditor();
const persistence = new InMemoryDocumentPersistence();
let filePath: string | null = "stale.ufo";
let dirty = true;
const document = new Document(editor, {
persistence,
createUntitledId: () => "untitled-1",
setFilePath: (nextPath) => {
filePath = nextPath;
Expand All @@ -49,7 +20,6 @@ function testDocument() {
return {
document,
editor,
persistence,
get filePath() {
return filePath;
},
Expand All @@ -71,7 +41,6 @@ describe("Document", () => {
});
expect(state.editor.font.loaded).toBe(true);
expect(state.editor.font.defaultSource.name).toBe("Regular");
expect(state.persistence.currentDocId).toBe("untitled-1");
expect(state.filePath).toBeNull();
expect(state.dirty).toBe(false);
});
Expand All @@ -90,7 +59,6 @@ describe("Document", () => {
name: "A",
unicode: 65,
});
expect(state.persistence.currentPath).toBe(MUTATORSANS_DESIGNSPACE);
expect(state.filePath).toBe(MUTATORSANS_DESIGNSPACE);
expect(state.dirty).toBe(false);
});
Expand All @@ -104,7 +72,6 @@ describe("Document", () => {
expect(state.document.identity).toBeNull();
expect(state.editor.font.loaded).toBe(false);
expect(state.editor.font.sources).toEqual([]);
expect(state.persistence.currentDocId).toBeNull();
expect(state.filePath).toBeNull();
});

Expand Down
26 changes: 1 addition & 25 deletions apps/desktop/src/renderer/src/app/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,22 @@ export type DocumentIdentity =
| { readonly kind: "file"; readonly path: string };

export interface DocumentServices {
readonly persistence: DocumentPersistence;
readonly setFilePath: (filePath: string | null) => void;
readonly clearDirty: () => void;
readonly createUntitledId?: () => string;
readonly notifySaveCompleted?: (path: string) => Promise<void> | void;
}

export interface DocumentPersistence {
closeDocument(): void;
openDocument(filePath: string): void;
openUntitledDocument(docId: string): void;
onDocumentPathChanged(filePath: string | null): void;
flushNow(): void;
}

/**
* App-level lifecycle for the current font document.
*
* `Document` owns the distinction between no document, a new untitled font,
* and a file-backed font. It coordinates editor font lifecycle, file identity,
* dirty state, and document-scoped persistence.
* and dirty state.
*/
export class Document {
readonly editor: Editor;

readonly #persistence: DocumentPersistence;
readonly #setFilePath: (filePath: string | null) => void;
readonly #clearDirty: () => void;
readonly #createUntitledId: () => string;
Expand All @@ -40,7 +30,6 @@ export class Document {

constructor(editor: Editor, services: DocumentServices) {
this.editor = editor;
this.#persistence = services.persistence;
this.#setFilePath = services.setFilePath;
this.#clearDirty = services.clearDirty;
this.#createUntitledId = services.createUntitledId ?? createUntitledId;
Expand All @@ -56,30 +45,20 @@ export class Document {
}

createFont(): void {
this.#persistence.closeDocument();

const id = this.#createUntitledId();
this.editor.createFont();
this.#identity = { kind: "untitled", id };

this.#setFilePath(null);
this.#clearDirty();

this.#persistence.openUntitledDocument(id);
this.#persistence.flushNow();
}

openFont(path: string): void {
this.#persistence.closeDocument();

this.editor.loadFont(path);
this.#identity = { kind: "file", path };

this.#setFilePath(path);
this.#clearDirty();

this.#persistence.openDocument(path);
this.#persistence.flushNow();
}

async saveFont(path?: string): Promise<void> {
Expand All @@ -94,8 +73,6 @@ export class Document {
this.#setFilePath(savePath);
this.#clearDirty();

this.#persistence.onDocumentPathChanged(savePath);
this.#persistence.flushNow();
await this.#notifySaveCompleted(savePath);
}

Expand All @@ -104,7 +81,6 @@ export class Document {
}

close(): void {
this.#persistence.closeDocument();
this.editor.closeFont();
this.#identity = null;
this.#setFilePath(null);
Expand Down
78 changes: 1 addition & 77 deletions apps/desktop/src/renderer/src/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,10 @@ import { TextRuns } from "@/lib/text/TextRuns";
import { TextRun, type FocusedGlyph } from "@/lib/text/TextRun";
import { glyphTextItem, Positioner } from "@/lib/text/layout";
import type { GlyphAnchor } from "@/lib/text/layout";
import { TextRunModuleSchema } from "@shift/validation";
import type { TextRunModule } from "@/persistence/types";

import type { ToolManifest, ToolShortcutEntry } from "@/types/tools";
import type { ToolStateScope } from "@/types/editor";
import { EventEmitter } from "./lifecycle";
import { StateRegistry, type ShiftState, type ShiftStateOptions } from "@/lib/state/ShiftState";

import type { LineSegmentPoints } from "@shift/glyph-state";
import type { ShiftBridge } from "@shift/bridge";
Expand Down Expand Up @@ -184,8 +181,6 @@ export class Editor {

#events: EventEmitter;

#stateRegistry: StateRegistry;

#textRuns: TextRuns;

#glyphFinderOpen: WritableSignal<boolean>;
Expand All @@ -207,7 +202,7 @@ export class Editor {
app: Map<string, unknown>;
document: Map<string, unknown>;
};
#toolStateVersion: WritableSignal<number>;

/**
* Initializes all subsystems, wires signal dependencies, and sets up
* reactive effects that schedule canvas redraws when state changes.
Expand All @@ -227,12 +222,10 @@ export class Editor {

this.#commandHistory = new CommandHistory(this.#glyph.edit.glyphSource);

this.#stateRegistry = new StateRegistry();
this.#toolState = {
app: new Map<string, unknown>(),
document: new Map<string, unknown>(),
};
this.#toolStateVersion = signal(0, { name: "editor.toolState.version" });

this.#glyphFinderOpen = signal(false, { name: "editor.glyphFinder.open" });

Expand Down Expand Up @@ -263,31 +256,6 @@ export class Editor {

this.#renderer = new Renderer(this);

const textRunPersistence = this.registerState<TextRunModule>({
id: "text-run",
scope: "document",
initial: () => ({ runsByGlyph: {} }),
serialize: () => ({ runsByGlyph: this.#textRuns.serialize() }),
deserialize: (json) => {
const payload = TextRunModuleSchema.parse(json);
this.#textRuns.deserialize(payload.runsByGlyph);
return payload;
},
});

// Bridge: when active run's buffer changes (or active switches), notify persistence.
effect(
() => {
const run = this.#textRuns.activeCell.value;
run.buffer.itemsCell.value;
run.buffer.cursorCell.value;
run.buffer.anchorCell.value;
run.buffer.originXCell.value;
textRunPersistence.set({ runsByGlyph: this.#textRuns.serialize() });
},
{ name: "editor.textRun.persistence" },
);

this.#events.on("fontLoaded", () => {
this.#commandHistory.clear();
this.#textRuns.clearAll();
Expand Down Expand Up @@ -422,10 +390,6 @@ export class Editor {
return this.#commandHistory.withBatch(label, fn);
}

public get toolStateVersionCell(): Signal<number> {
return this.#toolStateVersion;
}

public get debugOverlays(): DebugOverlays {
return this.#view.debugOverlaysCell.peek();
}
Expand Down Expand Up @@ -805,38 +769,12 @@ export class Editor {
const stateKey = this.#toolStateKey(toolId, key);
if (scopedState.get(stateKey) === value) return;
scopedState.set(stateKey, value);
this.#bumpToolStateVersion();
}

public deleteToolState(scope: ToolStateScope, toolId: string, key: string): void {
const scopedState = this.#getToolScopeMap(scope);
const stateKey = this.#toolStateKey(toolId, key);
if (!scopedState.delete(stateKey)) return;
this.#bumpToolStateVersion();
}

public exportToolState(scope: ToolStateScope): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const [key, value] of this.#getToolScopeMap(scope).entries()) {
out[key] = value;
}
return out;
}

public hydrateToolState(scope: ToolStateScope, state: Record<string, unknown>): void {
const scopedState = this.#getToolScopeMap(scope);
scopedState.clear();
for (const [key, value] of Object.entries(state)) {
scopedState.set(key, value);
}
this.#bumpToolStateVersion();
}

public clearToolState(scope: ToolStateScope): void {
const scopedState = this.#getToolScopeMap(scope);
if (scopedState.size === 0) return;
scopedState.clear();
this.#bumpToolStateVersion();
}

public get glyph(): Signal<Glyph | null> {
Expand All @@ -850,16 +788,6 @@ export class Editor {
/** Subscribe to a lifecycle event. Returns an unsubscribe function. */
public on: EventEmitter["on"] = (...args) => this.#events.on(...args);

/** Register persistent state. Returns a reactive handle for reading/writing. */
public registerState<T>(options: ShiftStateOptions<T>): ShiftState<T> {
return this.#stateRegistry.register(options);
}

/** @internal Used by persistence kernel. */
get stateRegistry(): StateRegistry {
return this.#stateRegistry;
}

public get commands(): CommandHistory {
return this.#commandHistory;
}
Expand Down Expand Up @@ -1423,8 +1351,4 @@ export class Editor {
#getToolScopeMap(scope: ToolStateScope): Map<string, unknown> {
return this.#toolState[scope];
}

#bumpToolStateVersion(): void {
this.#toolStateVersion.set(this.#toolStateVersion.peek() + 1);
}
}
1 change: 0 additions & 1 deletion apps/desktop/src/renderer/src/lib/signals/docs/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ cd apps/desktop && npm test
- `HoverManager` -- uses `$hoveredPointId`, `$hoveredSegmentId`, etc.
- `Selection` -- uses `WritableSignal` fields for selected point/anchor/segment state
- `NativeBridge` -- `$glyph` signal with `equals: () => false` for identity changes
- `ShiftState` -- uses `signal` for application-level reactive state
- `useSignalState` -- React bridge hook (in this module)
- `useSignalEffect` -- lifecycle-aware effect hook (in `@/hooks/useSignalEffect`)
- `CommandHistory` -- imports from reactive for undo/redo state signals
Loading
Loading