Skip to content

Workspace Panes — cdx_panes

cdx_panes is the IDE-style workspace shell used by the Vyuh CDX applications. It provides a Workspace widget that composes a header bar, navigation rail, three resizable docks (left / right / bottom), a body area with optional tabs, and a status bar — all driven by a single WorkspaceController with a JSON-serializable layout state.

The package also exports SplitPane and MultiSplitPane, the lower-level resizable layout primitives used by the workspace itself. Use those when you need only a local two-pane or N-pane split without the full IDE shell.

dart
import 'package:cdx_panes/cdx_panes.dart';

Workspace

dart
Workspace(
  controller: workspaceController,
  theme: const WorkspaceTheme(),
  headerBar: HeaderBar(title: const Text('My App'), trailing: const _UserMenu()),
  statusBar: const StatusBar(items: [...]),
  navigation: NavigationBar(entries: [...]),
  leftDock: Dock(
    placement: DockPlacement.left,
    panes: [
      Pane(id: 'explorer', title: 'Explorer', icon: FluentIcons.folder_24_regular, builder: (_) => const ExplorerPane()),
      Pane(id: 'search', title: 'Search', icon: FluentIcons.search_24_regular, builder: (_) => const SearchPane()),
    ],
  ),
  rightDock: Dock(placement: DockPlacement.right, panes: [...]),
  bottomDock: Dock(placement: DockPlacement.bottom, panes: [...]),
  body: WorkspaceBody(child: const EditorArea(), tabs: BodyTabs(items: [...])),
)

Dock + Pane

A Dock configures one of the three placements (left / right / bottom). Each dock owns one or more Panes, and the user switches between them via the dock's tab strip.

Dock paramPurpose
placementDockPlacement.left / .right / .bottom
panesList of Pane
defaultActivePaneIdPane to activate initially (defaults to first)
trailingStripActionWidget appended to the trailing end of the strip
resizableWhether the user can drag the edge (default true)
tabBarVisibilityauto / visible / hidden (default auto — hidden when single pane)
defaultModeDockMode.expanded / .collapsed / .hidden / .floating
defaultSize / minSize / maxSizePixel sizes
defaultSizeFraction / minSizeFraction / maxSizeFractionViewport-relative 0..1 — pairs with pixel values; the more permissive ceiling wins so docks grow on wider viewports
dart
Dock(
  placement: DockPlacement.right,
  defaultSize: 360,
  defaultSizeFraction: 0.30,         // 30% of viewport, but floored at 360 px
  panes: [
    Pane(
      id: 'inspector',
      title: 'Inspector',
      icon: FluentIcons.options_24_regular,
      builder: (_) => const InspectorPane(),
      headerActionsBuilder: (_) => Button.icon(icon: FluentIcons.pin_24_regular, ...),
      badge: const StatusBadge.compact(Colors.amber, label: 'Beta'),
      closable: true,
    ),
  ],
)

DockMode

dart
enum DockMode {
  hidden,    // not rendered at all
  collapsed, // tab strip only, content hidden
  expanded,  // strip + content, body shrinks (default)
  floating,  // content overlays body without consuming flex space
}

floating is for transient detail presentations (create/edit forms, focused inspectors) that should dismiss via navigation rather than user dock manipulation. The body keeps its full size; a non-interactive scrim covers it.

dart
NavigationBar(
  entries: [
    NavigationItem(id: 'inbox', label: 'Inbox', icon: FluentIcons.mail_inbox_24_regular, onTap: () => ...),
    NavigationItem(id: 'lots', label: 'Lots', icon: FluentIcons.box_24_regular, ...),
    const NavigationDivider(),
    NavigationGroup(label: 'Reports', children: [
      NavigationItem(id: 'reports.daily', label: 'Daily', icon: ...),
      NavigationItem(id: 'reports.monthly', label: 'Monthly', icon: ...),
    ]),
  ],
)

NavigationEntry is sealed:

VariantPurpose
NavigationItemTappable row (icon + label + optional badge / trailing)
NavigationGroupCollapsible section
NavigationDividerVisual separator

The bar's mode is driven by the controller (NavigationBarMode.hidden / .drawer / .pinnedCompact / .pinnedExpanded).

WorkspaceController

Owns dock runtime state (mode, size, active pane id) and exposes MobX observables. Read state via Observer; mutate via methods.

dart
final controller = WorkspaceController(
  navigation: navigationBar,
  leftDock: leftDock,
  rightDock: rightDock,
  bottomDock: bottomDock,
);

controller.setDockMode(DockPlacement.left, DockMode.collapsed);
controller.activatePane(DockPlacement.right, 'inspector');
controller.resizeDock(DockPlacement.bottom, 240);
controller.setNavigationMode(NavigationBarMode.pinnedCompact);
controller.resizeNavigation(280);

Layout persistence

WorkspaceConfig is the JSON-serializable snapshot — persist on the client (or server), restore on next launch.

dart
final config = controller.toConfig();              // WorkspaceConfig
final json = config.toJson();
await prefs.setString('workspace.layout', jsonEncode(json));

// later
final restored = WorkspaceConfig.fromJson(jsonDecode(stored));
controller.loadConfig(restored);                   // partial application

Built-in SharedPreferencesLayoutStorage adapter:

dart
controller.attachStorage(SharedPreferencesLayoutStorage(prefs, key: 'workspace.layout'));
// auto-saves on dock / nav changes

LayoutStorage is abstract — provide your own implementation for custom backends (server-side persistence, in-memory test doubles).

WorkspaceTheme

Visual styling tokens (sizes, icons, resizer thickness). Defaults are JetBrains-tight; override any subset to retune the workspace.

dart
Workspace(
  ...,
  theme: WorkspaceTheme(
    workspaceBackground: scheme.surface,
    resizerThickness: 6,
    iconSize: 18,
    stripThickness: 36,
  ),
)

WorkspaceScope

WorkspaceScope.of(context) exposes the controller, layout size, and theme to any descendant. Use it in custom panes that need to react to layout-size changes (small / medium / large) or trigger workspace mutations.

SplitPane / MultiSplitPane

SplitPane is a two-pane convenience wrapper around MultiSplitPane. It owns local drag state and reports ratio changes to the caller.

dart
SplitPane(
  axis: Axis.horizontal,
  initialRatio: 0.35,
  minFirstSize: 280,
  minSecondSize: 420,
  onRatioChanged: (ratio) => settings.saveExplorerRatio(ratio),
  first: const ExplorerPane(),
  second: const EditorPane(),
)

Use MultiSplitPane when you have three or more children, need controlled pixel sizes, or want to disable individual handles.

dart
MultiSplitPane(
  axis: Axis.horizontal,
  sizes: savedSizes,
  resizableHandles: const [true, false],
  onSizesChanged: settings.saveInspectorSizes,
  children: const [
    SplitPaneChild(weight: 1, minSize: 240, child: ExplorerPane()),
    SplitPaneChild(weight: 2, minSize: 400, child: EditorPane()),
    SplitPaneChild(weight: 1, minSize: 280, child: InspectorPane()),
  ],
)

MultiSplitPane requires bounded width for horizontal splits or bounded height for vertical splits. If sizes is provided, it is treated as the controlled pixel-size source and fitted to the current extent; otherwise weights seed the initial sizes and drag state is retained locally.

  • Layout & NavigationMasterDetailScaffold is the in-page alternative for simpler shells
  • PanelsCdxSlideInPanel for transient drilldown overlays