Skip to content

Latest commit

 

History

History
117 lines (94 loc) · 5.31 KB

File metadata and controls

117 lines (94 loc) · 5.31 KB

SplitViewController

The single owner of the split view structure: which node ids are open, how they are arranged, which pane has the focus, and the content revisions used to sync duplicated panes. It is a plain ChangeNotifier — no widget dependencies, fully unit-testable.

final SplitViewController controller = SplitViewController(
  maxPanes: 6,                      // optional hard limits
  maxColumns: 3,
  syncDuplicatedPaneContent: true,  // enable notifyNodeChanged
);

Insertion semantics

insertPane (and every drop) follows the zone semantics:

left   → new FIRST column (global edge, target ignored)   ── global
right  → new LAST column (global edge, target ignored)    ── global
top    → stacked above the target, same column            ── isolated
bottom → stacked below the target, same column            ── isolated
center → replaces the target pane content

When the view is empty, the first buffer takes the whole area no matter the zone.

Why do left/right ignore the target? There is only ONE left and ONE right in the whole split view (see Zones): they always create the new first/last column, so a target position would be meaningless.

Duplicate rules

The same node may be open several times, but never inserted at the exact position it already occupies:

  • top/center: rejected when the destination slot in the column already shows the node.
  • bottom: rejected when the slot right below the target already shows it.
  • left: rejected when the FIRST column is a single-pane column already showing it (the leftmost position would not change).
  • right: rejected when the LAST column is a single-pane column already showing it. A single column holds the LEFT position (buffers fill from the left), so its right side is vacant and accepts.
  • Lone-buffer exception: a single full-size pane accepts its own node on left/right/top — with one pane, both sides of each axis are still open positions.

Why "same position" instead of "no duplicates"? Viewing one document in two panes is a core of the most common writing softwares workflow. The only insert that makes no sense is the one that changes nothing — so that is the only one rejected. canInsertAt exposes the exact same verdict the drop shadows use, so UI and behavior can never disagree.

Limits

  • maxPanes gates every additive insertion (center replacements are free — they do not increase the count).
  • maxColumns gates only the operations that CREATE columns: left/right insertions and open.
  • isPaneLimitReached / isColumnLimitReached are public so app UI can react (e.g. disable buttons).

movePane is net-zero aware: because it removes first and validates after, moving a pane at the limit works, while moves that would exceed a limit are restored atomically.

Operations

Operation Notes
open(nodeId) New column at the end. Gated by both limits.
insertPane(column, pane, zone, nodeId) Zone semantics above; silent no-op when invalid.
canInsertAt(nodeId, column, pane, zone) The validation the shadows use.
closePane(column, pane) / close(nodeId) / closeAll() Empty columns are removed; focus is reconciled.
movePane(from…, to…, zone) → bool Moves the SplitPane OBJECT: weight + widget state travel with it. Atomic restore on invalid destinations.
canMovePane(…) → bool Dry-run of movePane: same simulation, state always restored, no notify. Used for honest drop shadows.
swapPanes(from…, to…) → bool Pure permutation — nothing opens or closes; both identities travel. The dragged pane keeps the focus.
focusPane(column, pane) / focused Focus as a (column, pane) record.
locate(nodeId) / locatePaneById(paneId) / isOpen / paneCountOf Lookups. Pane ids are the stable way to find a pane after structure changes.

Why does movePane remove-then-validate? The validity of a destination depends on the state without the moved pane (indices shift, columns may collapse). Simulating removal, validating, and restoring on failure keeps one single code path for movePane and canMovePane (_tryMovePane(commit: …)) — no duplicated rule logic that could drift apart.

No-op discipline: every operation that changes nothing emits no notification. Rebuilds are never free; the controller refuses to trigger them gratuitously.

Content revisions (duplicated-pane sync)

With syncDuplicatedPaneContent: true, calling notifyNodeChanged(nodeId) after an edit:

  1. Bumps the node's revision (nodeRevisionOf).
  2. Notifies listeners only when 2+ panes show that node — a lone pane has nobody to sync with, so typing in a unique document costs zero rebuilds.
  3. Every pane of that node receives the new PaneContext.revision; compare it in didUpdateWidget to reload external edits.

Revisions are transient sync state: they die with the last pane of their node (closePane/closeAll clean them).

Why revisions instead of content? The library never owns content (see Architecture). It transports the signal; what "reloading" means belongs to your widgets. If you prefer a fully app-owned mechanism, see the store pattern in Recipes — both approaches are supported.