Skip to content

Add split view to the Explorer (two panes side by side) - #517

Open
jaysuk wants to merge 2 commits into
Duet3D:v3.7-devfrom
jaysuk:explorer-split-view
Open

jaysuk wants to merge 2 commits into
Duet3D:v3.7-devfrom
jaysuk:explorer-split-view

Conversation

@jaysuk

@jaysuk jaysuk commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a split view to the Explorer page: a pane can be split into two independent tab strips with a draggable divider between them, so two files (or a file and a directory listing) can be open side by side - e.g. reference one macro while editing another, or compare two config files.

  • Split right button (in the toolbar, next to "+") creates a second pane and moves the active tab into it. Enabled once there are 2+ tabs to split from.
  • Close split merges the second pane's tabs back into the first and removes the split.
  • Each pane behaves exactly like the pre-split single tab strip: its own tabs, its own drag-and-drop reordering, its own "+" button.
  • Tabs can be dragged from one pane's strip onto the other to move them across.
  • The divider is draggable to resize the panes live; the ratio persists across sessions (cacheStore.explorerSplitRatio), mirroring the existing nav-drawer resize handle.
  • Below the lg breakpoint the non-focused pane is hidden (not unmounted) and the split controls are hidden, so it degrades to the current single-pane behaviour on narrower viewports without losing state. (Originally gated on md; raised to lg since two side-by-side tab strips need more room than a typical tablet-width window gives them.)

Implementation notes

  • Kept the existing flat tabs array and its URL-sync/drag/close logic largely intact by annotating each tab with a groupId (which pane it's in), rather than restructuring into per-pane arrays. groups is capped at exactly two fixed ids, so there's nothing to grow or clean up.
  • setActiveTab() replaces direct activeTab.value writes and tracks which pane last had focus (focusedGroupId); the URL sync, scroll-restore watcher, and new-tab placement all key off it.
  • moveTabToGroup() / splitRight() / closeSplit() handle pane transitions, reusing the existing closeTab fallback-tab-selection logic so closing/moving the active tab in a pane always lands on a sensible sibling.
  • useGcodeThumbnails() is single-flight internally, so it's now instantiated once per pane rather than once for the whole page - otherwise two panes browsing different /gcodes directories would cancel each other's fetch and share one progress bar.
  • The non-focused pane collapses via a CSS class on narrow viewports rather than v-if, since MonacoEditor's unmount has no unsaved-content flush path - unmounting a dirty editor would silently lose the edits.
  • Browser back/forward ordinal resolution is scoped to the focused pane's own tabs, so a stray ordinal can never resolve into - and steal focus from - the other, non-focused pane.

Testing

  • vue-tsc --noEmit and npm run build both pass, including after the md -> lg breakpoint change.
  • Manually tested against a real Duet board: opened two macros side by side and dragged the divider to resize the panes.
  • Also manually driven end-to-end against the dev server with Playwright (no live board): opened tabs, split, dragged the divider, dragged a tab across panes, closed the split, and shrank the viewport below the breakpoint - all confirmed via screenshots and DOM assertions, zero console/page errors throughout. That pass predates the md -> lg change; the collapse behaviour itself is unchanged, just the threshold it fires at, but it hasn't been re-run specifically against lg.
  • Not yet exercised on real hardware: the narrow-viewport collapse, thumbnails against actual /gcodes directories in split panes, and the dirty-editor-survives-a-resize case.

🤖 Generated with Claude Code

jaysuk and others added 2 commits August 17, 2026 15:03
Lets a pane be split into two independent tab strips with a draggable
divider, so two files (or a file and a directory listing) can be open
side by side - e.g. reference one macro while editing another.

Implementation keeps the existing flat `tabs` array and URL/drag/close
logic largely intact by annotating each tab with which pane (group) it
belongs to, rather than restructuring tabs into per-pane arrays:

- groups: [{ id, activeTabId }], capped at exactly two fixed ids so
  there's nothing to grow or clean up
- setActiveTab() replaces direct activeTab.value writes and tracks
  which pane last had focus (focusedGroupId), which the URL sync,
  scroll-restore watcher, and new-tab placement all key off
- moveTabToGroup()/splitRight()/closeSplit() handle pane transitions,
  reusing closeTab's fallback-tab selection logic
- Per-pane thumbnail fetchers (useGcodeThumbnails is single-flight)
  so two panes browsing different /gcodes directories don't cancel
  each other's fetch
- The non-focused pane collapses via CSS on narrow viewports rather
  than unmounting, since MonacoEditor.dispose() has no unsaved-content
  flush path
- Browser back/forward ordinal resolution is scoped to the focused
  pane's own tabs so it can never reveal into or steal focus from the
  other, non-focused pane

Divider drag mirrors the existing nav-drawer resize handle in
layouts/builtin.vue; the split ratio persists via a new
cacheStore.explorerSplitRatio field the same way menuWidth does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two side-by-side tab strips need more room than the md threshold
(840px) gives them on a typical tablet-width window; lg leaves the
split controls and pane collapse gated on a genuinely wide viewport.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jaysuk

jaysuk commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

recheck

@chrishamm

chrishamm commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for the PR, but I can't merge this as it is.

Every pane transition (split right, close split, dragging a tab across, and the group id rename in collapseEmptyGroup) moves a v-window-item between two v-window parents. Vue cannot reparent that, so the MonacoEditor is unmounted and remounted, which disposes the model and re-downloads the file. Any unsaved edits are lost silently, and none of these paths checks dirty. The URL has the same problem in the other direction: it carries only path and ordinal, no pane, so after focusing pane 2 a browser Back resolves the ordinal in pane 2 and overwrites its directory instead of refocusing pane 1.

To make this work the Monaco model needs to belong to the tab rather than the editor instance (a model cache keyed by filename, setModel on mount, dispose when the tab closes), and the URL needs a pane discriminator or history has to be limited to the first pane. Apart from that this is too big for the rc phase of 3.7, so please rebase it on next once that is fixed.

@chrishamm chrishamm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this to changes requested so the state matches my comment above. The Monaco
model has to belong to the tab before the pane moves are safe, and this is too big
for the 3.7 rc phase regardless, so please rebase on next once that is sorted.

jaysuk added a commit to jaysuk/dwc-gcode-editor that referenced this pull request Sep 29, 2026
…lit panes

- workspace: collapseEmptyGroup removes an emptied pane without changing any tab id
- workspace header documents the rule that keeps editors mounted across pane moves:
  render tabs as one flat list keyed by id, in id order, placed by CSS grid column
  (the remount flaw Duet3D/DuetWebControl#517 was sent back for)
- 358 tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
jaysuk added a commit to jaysuk/Flexible-Layouts that referenced this pull request Sep 29, 2026
…v0.13.0)

Split right / Close split, a draggable divider, drag a tab across, focus follows a click.
Every tab renders flat in one CSS grid (never nested per pane) so an editor is never
remounted when its tab changes pane - unsaved edits, undo and scroll survive
(Duet3D/DuetWebControl#517 was sent back for exactly that). The URL follows the focused
pane's file and resolves by path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants