Skip to content

Undo should be scoped to the current glyph or authoring item #114

Description

@kostyafarber

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:

  1. Open glyph A and edit its geometry.
  2. Return to the glyph grid.
  3. Open glyph B.
  4. Press undo while looking at glyph B.
  5. 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions