Problem
Undo is currently backed by the workspace-wide Rust ledger. That means undo answers: undo the latest edit in the workspace.
This creates spooky behavior when moving between glyph grid items:
- Open glyph A and edit its geometry.
- Return to the glyph grid.
- Open glyph B.
- Press undo while looking at glyph B.
- The app can undo the latest edit from glyph A.
This is not about native windows. The user intent follows the current glyph/item being authored, not the BrowserWindow.
Expected behavior
Undo from glyph B should only affect edits relevant to glyph B, or do nothing if glyph B has no undoable edits.
It should not jump across to glyph A simply because glyph A has the most recent document-wide ledger entry.
Proposed direction
Make undo/redo a scoped history query instead of a single global stack pop.
Every persisted edit should record an edit scope when it enters the Rust ledger, for example:
type EditScope =
| { kind: "layer"; layerId: LayerId }
| { kind: "glyph"; glyphId: GlyphId }
| { kind: "source"; sourceId: SourceId }
| { kind: "workspace" };
Likely mapping:
- Point, segment, contour, anchor, advance edits:
layer
- Glyph metadata, name, unicode edits:
glyph
- Source, axis, variation edits:
source or workspace
- Add/delete glyph: probably
workspace, but needs a product decision
- Future scene node edits: separate scene/node scope if persisted
JS/editor should resolve the current undo scope from the current scene glyph node plus active source/layer, then call into the workspace API with that scope.
Rust should own the actual matching and replay because Rust owns the canonical ledger and state-pair undo semantics.
Rust/API shape
Instead of:
workspace.undo()
workspace.redo()
move toward:
workspace.undo({ scope })
workspace.redo({ scope })
The Rust ledger should find the latest undoable entry matching the requested scope and replay that entry. It should not pop unrelated entries.
Redo needs the same scoped model. A simple global redo stack will get weird once undo can skip unrelated entries. One possible structure is a ledger list where entries track done/undone state:
type LedgerEntry = {
id: LedgerEntryId;
label: string;
scope: EditScope;
status: "done" | "undone";
steps: LedgerStep[];
};
A new edit should probably invalidate redo entries for the same scope, not necessarily the whole workspace.
Why this belongs in Rust
For persisted font edits, JS should not maintain parallel per-glyph undo stacks. That would duplicate history and make JS guess which Rust ledger entry to pop.
The editor should decide the current authoring scope. Rust should record scopes, search history, replay state pairs, and return AppliedChange as it does today so FontStore can keep folding returned state.
Open questions
- Should glyph geometry scope be
layer or glyph for user-facing undo?
- How should workspace-wide operations interact with scoped glyph undo?
- What is the redo invalidation policy across scopes?
- Should creating a glyph be undoable from that glyph after opening it, or only from workspace/global scope?
- How should future scene-only nodes participate if they are persisted outside the font layer model?
Problem
Undo is currently backed by the workspace-wide Rust ledger. That means undo answers: undo the latest edit in the workspace.
This creates spooky behavior when moving between glyph grid items:
This is not about native windows. The user intent follows the current glyph/item being authored, not the BrowserWindow.
Expected behavior
Undo from glyph B should only affect edits relevant to glyph B, or do nothing if glyph B has no undoable edits.
It should not jump across to glyph A simply because glyph A has the most recent document-wide ledger entry.
Proposed direction
Make undo/redo a scoped history query instead of a single global stack pop.
Every persisted edit should record an edit scope when it enters the Rust ledger, for example:
Likely mapping:
layerglyphsourceorworkspaceworkspace, but needs a product decisionJS/editor should resolve the current undo scope from the current scene glyph node plus active source/layer, then call into the workspace API with that scope.
Rust should own the actual matching and replay because Rust owns the canonical ledger and state-pair undo semantics.
Rust/API shape
Instead of:
move toward:
The Rust ledger should find the latest undoable entry matching the requested scope and replay that entry. It should not pop unrelated entries.
Redo needs the same scoped model. A simple global redo stack will get weird once undo can skip unrelated entries. One possible structure is a ledger list where entries track done/undone state:
A new edit should probably invalidate redo entries for the same scope, not necessarily the whole workspace.
Why this belongs in Rust
For persisted font edits, JS should not maintain parallel per-glyph undo stacks. That would duplicate history and make JS guess which Rust ledger entry to pop.
The editor should decide the current authoring scope. Rust should record scopes, search history, replay state pairs, and return
AppliedChangeas it does today soFontStorecan keep folding returned state.Open questions
layerorglyphfor user-facing undo?