From 11eb91effef8a98c3a0333de5e01fcb99b70635b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Apr 2026 22:47:45 +0200 Subject: [PATCH] implement collapse spine, focus mode, keyboard shortcuts, context rail Collapsed side panels render as a 12px vertical spine with rotated label, hover highlight, and badge dot (D-051). Focus mode snapshots and restores the full layout arrangement (D-052). Fourteen new commands in the default-layout extension: collapse toggles (Ctrl+Shift+1/3), panel focus (Ctrl+1/2/3), focus mode (Ctrl+.), sidebar section switching (Alt+1-5), Escape dismiss (D-054). Right panel gets a bottom icon rail matching the sidebar pattern (D-047). Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 22 +++ app/lib/app.dart | 96 +++++++++-- .../builtin/default_layout/src/extension.dart | 161 +++++++++++++++--- app/lib/kernel/src/panels/arrangement.dart | 74 ++++++-- app/lib/widgets/src/clide_spine.dart | 94 ++++++++++ app/lib/widgets/widgets.dart | 1 + .../kernel/src/panels/arrangement_test.dart | 54 ++++++ 7 files changed, 456 insertions(+), 46 deletions(-) create mode 100644 app/lib/widgets/src/clide_spine.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index d98c49ff..64782e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,28 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit. ### Added +- Interaction model from Wireframe Flows v3: eight new D-records + (D-047 through D-054) and five Q-records (Q-026 through Q-030) + codifying layout invariants, chrome budget, editor mode, context + auto-behavior, collapse spine, focus mode, state persistence, and + the canonical keyboard map. + +- Panel collapse spine — collapsed side panels render as a 12px + vertical spine with rotated label, hover highlight, and badge dot + for pending context (D-051, T-030). + +- Focus mode — `Ctrl+.` takes the active panel full-window; + `Escape` restores the prior layout with collapse states and + divider positions intact (D-052, T-031). + +- Canonical keyboard shortcuts from the interaction model: collapse + toggles (`Ctrl+Shift+1/3`), panel focus (`Ctrl+1/2/3`), sidebar + section switching (`Alt+1–5`), focus mode, and `Escape` dismiss + (D-054, T-033). + +- Right panel (context) icon rail — bottom section switcher matching + the left sidebar rail pattern (D-047, T-034). + - Syntax highlighting via tree-sitter (dart:ffi to vendored libtree-sitter.so with embedded wasmtime). 48 grammar WASM files, 48 highlight queries. Colors map to theme syntax tokens. diff --git a/app/lib/app.dart b/app/lib/app.dart index c46f72f6..57c3dcf8 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -30,8 +30,7 @@ class _AppRoot extends StatelessWidget { return WidgetsApp( title: 'clide', color: const Color(0xFF000000), - pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) => - PageRouteBuilder( + pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) => PageRouteBuilder( settings: settings, pageBuilder: (ctx, _, __) => builder(ctx), ), @@ -117,7 +116,9 @@ class RootLayout extends StatelessWidget { builder: (ctx, _) { final a = kernel.arrangement; final sidebarVisible = a.isVisible(Slots.sidebar); + final sidebarCollapsed = a.isCollapsed(Slots.sidebar); final contextVisible = a.isVisible(Slots.contextPanel); + final contextCollapsed = a.isCollapsed(Slots.contextPanel); final statusVisible = a.isVisible(Slots.statusbar); final sidebarSize = a.sizeOf(Slots.sidebar) ?? 240; final contextSize = a.sizeOf(Slots.contextPanel) ?? 280; @@ -128,7 +129,13 @@ class RootLayout extends StatelessWidget { Expanded( child: Row( children: [ - if (sidebarVisible) ...[ + if (sidebarVisible && sidebarCollapsed) + ClideSpine( + label: _sidebarSpineLabel(kernel), + side: SpineSide.left, + onExpand: () => a.setCollapsed(Slots.sidebar, false), + ) + else if (sidebarVisible) ...[ SizedBox( width: sidebarSize, child: SlotHost(slot: Slots.sidebar), @@ -140,7 +147,13 @@ class RootLayout extends StatelessWidget { ), ], const Expanded(child: SlotHost(slot: Slots.workspace)), - if (contextVisible) ...[ + if (contextVisible && contextCollapsed) + ClideSpine( + label: 'context', + side: SpineSide.right, + onExpand: () => a.setCollapsed(Slots.contextPanel, false), + ) + else if (contextVisible) ...[ DragResizeHandle( arrangement: a, slot: Slots.contextPanel, @@ -164,6 +177,16 @@ class RootLayout extends StatelessWidget { }, ); } + + static String _sidebarSpineLabel(KernelServices kernel) { + final activeTab = kernel.panels.activeTabIn(Slots.sidebar); + if (activeTab == null) return 'overview'; + final tabs = kernel.panels.tabsFor(Slots.sidebar); + for (final t in tabs) { + if (t.id == activeTab) return t.title.toLowerCase(); + } + return 'overview'; + } } class SlotHost extends StatelessWidget { @@ -196,14 +219,22 @@ class SlotHost extends StatelessWidget { ); } + if (slot == Slots.contextPanel) { + return _ContextSlot( + tabs: tabs, + active: active, + activeId: activeId, + onSelect: (id) => kernel.panels.activateTab(slot, id), + ); + } + return Container( color: tokens.panelBackground, child: Column( children: [ ClideTabBar( items: [ - for (final t in tabs) - ClideTabItem(id: t.id, title: _resolveTitle(ctx, t)), + for (final t in tabs) ClideTabItem(id: t.id, title: _resolveTitle(ctx, t)), ], activeId: active.id, onSelect: (id) => kernel.panels.activateTab(slot, id), @@ -279,6 +310,54 @@ class _SidebarSlot extends StatelessWidget { } } +class _ContextSlot extends StatelessWidget { + const _ContextSlot({ + required this.tabs, + required this.active, + required this.activeId, + required this.onSelect, + }); + + final List tabs; + final TabContribution active; + final String activeId; + final ValueChanged onSelect; + + @override + Widget build(BuildContext context) { + final tokens = ClideTheme.of(context).surface; + return Container( + color: tokens.panelBackground, + child: Column( + children: [ + Expanded(child: active.build(context)), + ClideIconRail( + items: [ + for (final t in tabs) + ClideIconRailItem( + id: t.id, + icon: _iconFor(t), + tooltip: SlotHost._resolveTitle(context, t), + ), + ], + activeId: activeId, + onSelect: onSelect, + ), + ], + ), + ); + } + + static ClideIconPainter _iconFor(TabContribution t) { + if (t.icon is ClideIconPainter) return t.icon as ClideIconPainter; + return switch (t.id) { + 'markdown.viewer' => const DotIcon(), + 'graph.view' => const SearchIcon(), + _ => const DotIcon(), + }; + } +} + class StatusbarHost extends StatelessWidget { const StatusbarHost({super.key}); @@ -289,10 +368,7 @@ class StatusbarHost extends StatelessWidget { return ListenableBuilder( listenable: kernel.panels, builder: (ctx, _) { - final items = kernel.panels - .contributionsFor(Slots.statusbar) - .whereType() - .toList(); + final items = kernel.panels.contributionsFor(Slots.statusbar).whereType().toList(); final left = items.where((i) => i.priority < 100).toList(); final right = items.where((i) => i.priority >= 100).toList(); return Container( diff --git a/app/lib/builtin/default_layout/src/extension.dart b/app/lib/builtin/default_layout/src/extension.dart index 44947bb1..5f6d311c 100644 --- a/app/lib/builtin/default_layout/src/extension.dart +++ b/app/lib/builtin/default_layout/src/extension.dart @@ -8,7 +8,7 @@ class DefaultLayoutExtension extends ClideExtension { @override String get title => 'Default layout'; @override - String get version => '0.1.0'; + String get version => '0.2.0'; LayoutPresetContribution? _preset; ClideExtensionContext? _ctx; @@ -29,20 +29,67 @@ class DefaultLayoutExtension extends ClideExtension { defaultBinding: 'ctrl+shift+p', run: _togglePalette, ), + // Collapse toggles (D-051, D-054) CommandContribution( - id: 'sidebar.toggle', - command: 'sidebar.toggle', - title: 'Toggle Sidebar', - defaultBinding: 'ctrl+b', - run: _toggleSidebar, + id: 'sidebar.collapse', + command: 'sidebar.collapse', + title: 'Toggle Sidebar Collapse', + defaultBinding: 'ctrl+shift+1', + run: _collapseSidebar, ), CommandContribution( - id: 'context.toggle', - command: 'context.toggle', - title: 'Toggle Context Panel', - defaultBinding: 'ctrl+j', - run: _toggleContext, + id: 'context.collapse', + command: 'context.collapse', + title: 'Toggle Context Panel Collapse', + defaultBinding: 'ctrl+shift+3', + run: _collapseContext, ), + // Panel focus (D-054) + CommandContribution( + id: 'panel.focus.left', + command: 'panel.focus.left', + title: 'Focus Left Panel', + defaultBinding: 'ctrl+1', + run: _focusLeft, + ), + CommandContribution( + id: 'panel.focus.middle', + command: 'panel.focus.middle', + title: 'Focus Middle Panel', + defaultBinding: 'ctrl+2', + run: _focusMiddle, + ), + CommandContribution( + id: 'panel.focus.right', + command: 'panel.focus.right', + title: 'Focus Right Panel', + defaultBinding: 'ctrl+3', + run: _focusRight, + ), + // Focus mode (D-052, D-054) + CommandContribution( + id: 'panel.focusMode', + command: 'panel.focusMode', + title: 'Toggle Focus Mode', + defaultBinding: 'ctrl+.', + run: _toggleFocusMode, + ), + CommandContribution( + id: 'panel.focusMode.exit', + command: 'panel.focusMode.exit', + title: 'Exit Focus Mode', + defaultBinding: 'escape', + run: _exitFocusMode, + ), + // Sidebar section switching (D-054): alt+1 through alt+5 + for (var i = 0; i < 5; i++) + CommandContribution( + id: 'sidebar.section.${i + 1}', + command: 'sidebar.section.${i + 1}', + title: 'Sidebar: Section ${i + 1}', + defaultBinding: 'alt+${i + 1}', + run: (args) => _switchSidebarSection(i), + ), ]; @override @@ -56,9 +103,7 @@ class DefaultLayoutExtension extends ClideExtension { Future _reset(List args) async { final preset = _preset; final ctx = _ctx; - if (preset == null || ctx == null) { - return _notActivated(); - } + if (preset == null || ctx == null) return _notActivated(); ctx.arrangement.applyPreset(preset); return IpcResponse.ok(id: '', data: {'preset': preset.id}); } @@ -70,20 +115,92 @@ class DefaultLayoutExtension extends ClideExtension { return IpcResponse.ok(id: '', data: {'open': ctx.palette.isOpen}); } - Future _toggleSidebar(List args) async { + Future _collapseSidebar(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); - final visible = ctx.arrangement.isVisible(Slots.sidebar); - ctx.arrangement.setVisible(Slots.sidebar, !visible); - return IpcResponse.ok(id: '', data: {'visible': !visible}); + ctx.arrangement.toggleCollapsed(Slots.sidebar); + final collapsed = ctx.arrangement.isCollapsed(Slots.sidebar); + return IpcResponse.ok(id: '', data: {'collapsed': collapsed}); } - Future _toggleContext(List args) async { + Future _collapseContext(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); - final visible = ctx.arrangement.isVisible(Slots.contextPanel); - ctx.arrangement.setVisible(Slots.contextPanel, !visible); - return IpcResponse.ok(id: '', data: {'visible': !visible}); + ctx.arrangement.toggleCollapsed(Slots.contextPanel); + final collapsed = ctx.arrangement.isCollapsed(Slots.contextPanel); + return IpcResponse.ok(id: '', data: {'collapsed': collapsed}); + } + + Future _focusLeft(List args) async { + final ctx = _ctx; + if (ctx == null) return _notActivated(); + if (ctx.arrangement.isCollapsed(Slots.sidebar)) { + ctx.arrangement.setCollapsed(Slots.sidebar, false); + } + final active = ctx.panels.activeTabIn(Slots.sidebar); + if (active != null) { + ctx.focus.setActive(slot: Slots.sidebar, contributionId: active); + } + return IpcResponse.ok(id: '', data: {'focused': 'sidebar'}); + } + + Future _focusMiddle(List args) async { + final ctx = _ctx; + if (ctx == null) return _notActivated(); + final active = ctx.panels.activeTabIn(Slots.workspace); + if (active != null) { + ctx.focus.setActive(slot: Slots.workspace, contributionId: active); + } + return IpcResponse.ok(id: '', data: {'focused': 'workspace'}); + } + + Future _focusRight(List args) async { + final ctx = _ctx; + if (ctx == null) return _notActivated(); + if (ctx.arrangement.isCollapsed(Slots.contextPanel)) { + ctx.arrangement.setCollapsed(Slots.contextPanel, false); + } + final active = ctx.panels.activeTabIn(Slots.contextPanel); + if (active != null) { + ctx.focus.setActive(slot: Slots.contextPanel, contributionId: active); + } + return IpcResponse.ok(id: '', data: {'focused': 'context'}); + } + + Future _toggleFocusMode(List args) async { + final ctx = _ctx; + if (ctx == null) return _notActivated(); + final activeSlot = ctx.focus.activeSlot ?? Slots.workspace; + ctx.arrangement.toggleFocusMode(activeSlot); + return IpcResponse.ok(id: '', data: {'focusMode': ctx.arrangement.isInFocusMode}); + } + + Future _exitFocusMode(List args) async { + final ctx = _ctx; + if (ctx == null) return _notActivated(); + if (ctx.arrangement.isInFocusMode) { + ctx.arrangement.exitFocusMode(); + return IpcResponse.ok(id: '', data: {'focusMode': false}); + } + if (ctx.palette.isOpen) { + ctx.palette.toggle(); + return IpcResponse.ok(id: '', data: {'palette': false}); + } + return IpcResponse.ok(id: '', data: {}); + } + + Future _switchSidebarSection(int index) async { + final ctx = _ctx; + if (ctx == null) return _notActivated(); + if (ctx.arrangement.isCollapsed(Slots.sidebar)) { + ctx.arrangement.setCollapsed(Slots.sidebar, false); + } + final tabs = ctx.panels.tabsFor(Slots.sidebar); + if (index < tabs.length) { + ctx.panels.activateTab(Slots.sidebar, tabs[index].id); + return IpcResponse.ok(id: '', data: {'section': tabs[index].id}); + } + return IpcResponse.ok(id: '', data: {}); } static IpcResponse _notActivated() => IpcResponse.err( diff --git a/app/lib/kernel/src/panels/arrangement.dart b/app/lib/kernel/src/panels/arrangement.dart index 784c0203..d2c34437 100644 --- a/app/lib/kernel/src/panels/arrangement.dart +++ b/app/lib/kernel/src/panels/arrangement.dart @@ -3,19 +3,18 @@ import 'package:clide_app/kernel/src/panels/registry.dart'; import 'package:clide_app/kernel/src/panels/slot_id.dart'; import 'package:flutter/foundation.dart'; -/// Current, runtime layout state: which slots are visible, at what size, -/// and in what positions. Persisted through `settings`. -/// -/// The registry knows which slots *exist* and which contributions are -/// mounted. The arrangement knows which slots are *currently* shown and -/// how big they are — user-modifiable via drag-resize. class LayoutArrangement extends ChangeNotifier { LayoutArrangement(); final Map _state = {}; + Map? _focusModeSnapshot; + SlotId? _focusModeSlot; + void applyPreset(LayoutPresetContribution preset) { _state.clear(); + _focusModeSnapshot = null; + _focusModeSlot = null; for (final slot in preset.slots) { _state[slot.slot] = _SlotState( position: slot.position, @@ -35,12 +34,14 @@ class LayoutArrangement extends ChangeNotifier { double? minSizeOf(SlotId id) => _state[id]?.minSize; double? maxSizeOf(SlotId id) => _state[id]?.maxSize; bool isVisible(SlotId id) => _state[id]?.visible ?? false; + bool isCollapsed(SlotId id) => _state[id]?.collapsed ?? false; + bool get isInFocusMode => _focusModeSlot != null; + SlotId? get focusModeSlot => _focusModeSlot; void setSize(SlotId id, double size) { final s = _state[id]; if (s == null) return; - final clamped = - size.clamp(s.minSize ?? 0, s.maxSize ?? double.infinity).toDouble(); + final clamped = size.clamp(s.minSize ?? 0, s.maxSize ?? double.infinity).toDouble(); if (s.size == clamped) return; _state[id] = s.copyWith(size: clamped); notifyListeners(); @@ -53,12 +54,53 @@ class LayoutArrangement extends ChangeNotifier { notifyListeners(); } - /// Convenience for the default-layout extension: register all slots - /// it provides into a [PanelRegistry] with defaults from this preset. - void registerSlotsInto( - PanelRegistry registry, - LayoutPresetContribution preset, - ) { + void setCollapsed(SlotId id, bool collapsed) { + final s = _state[id]; + if (s == null || s.collapsed == collapsed) return; + _state[id] = s.copyWith(collapsed: collapsed); + notifyListeners(); + } + + void toggleCollapsed(SlotId id) { + final s = _state[id]; + if (s == null) return; + _state[id] = s.copyWith(collapsed: !s.collapsed); + notifyListeners(); + } + + void enterFocusMode(SlotId slot) { + if (_focusModeSlot != null) return; + _focusModeSnapshot = {for (final e in _state.entries) e.key: e.value}; + _focusModeSlot = slot; + for (final id in _state.keys) { + if (id == slot) { + _state[id] = _state[id]!.copyWith(visible: true, collapsed: false); + } else { + _state[id] = _state[id]!.copyWith(visible: false); + } + } + notifyListeners(); + } + + void exitFocusMode() { + final snap = _focusModeSnapshot; + if (snap == null) return; + _state.clear(); + _state.addAll(snap); + _focusModeSnapshot = null; + _focusModeSlot = null; + notifyListeners(); + } + + void toggleFocusMode(SlotId slot) { + if (_focusModeSlot != null) { + exitFocusMode(); + } else { + enterFocusMode(slot); + } + } + + void registerSlotsInto(PanelRegistry registry, LayoutPresetContribution preset) { for (final slot in preset.slots) { registry.registerSlot(SlotDefinition( id: slot.slot, @@ -78,6 +120,7 @@ class _SlotState { this.minSize, this.maxSize, this.visible = true, + this.collapsed = false, }); final SlotPosition position; @@ -85,6 +128,7 @@ class _SlotState { final double? minSize; final double? maxSize; final bool visible; + final bool collapsed; _SlotState copyWith({ SlotPosition? position, @@ -92,6 +136,7 @@ class _SlotState { double? minSize, double? maxSize, bool? visible, + bool? collapsed, }) { return _SlotState( position: position ?? this.position, @@ -99,6 +144,7 @@ class _SlotState { minSize: minSize ?? this.minSize, maxSize: maxSize ?? this.maxSize, visible: visible ?? this.visible, + collapsed: collapsed ?? this.collapsed, ); } } diff --git a/app/lib/widgets/src/clide_spine.dart b/app/lib/widgets/src/clide_spine.dart new file mode 100644 index 00000000..b2a3383c --- /dev/null +++ b/app/lib/widgets/src/clide_spine.dart @@ -0,0 +1,94 @@ +import 'dart:math' as math; + +import 'package:clide_app/kernel/src/theme/controller.dart'; +import 'package:flutter/widgets.dart'; + +class ClideSpine extends StatefulWidget { + const ClideSpine({ + super.key, + required this.label, + required this.onExpand, + this.side = SpineSide.left, + this.badgeCount = 0, + }); + + final String label; + final VoidCallback onExpand; + final SpineSide side; + final int badgeCount; + + static const double width = 12; + + @override + State createState() => _ClideSpineState(); +} + +enum SpineSide { left, right } + +class _ClideSpineState extends State { + bool _hovered = false; + + @override + Widget build(BuildContext context) { + final tokens = ClideTheme.of(context).surface; + final borderSide = BorderSide(color: tokens.dividerColor); + + return Semantics( + button: true, + label: '${widget.label} — click to expand', + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: GestureDetector( + onTap: widget.onExpand, + child: Container( + width: ClideSpine.width, + decoration: BoxDecoration( + color: _hovered ? tokens.sidebarItemHover : tokens.sidebarBackground, + border: Border( + left: widget.side == SpineSide.right ? borderSide : BorderSide.none, + right: widget.side == SpineSide.left ? borderSide : BorderSide.none, + ), + ), + child: Stack( + children: [ + Center( + child: Transform.rotate( + angle: widget.side == SpineSide.left ? -math.pi / 2 : math.pi / 2, + child: Text( + widget.label, + style: TextStyle( + fontSize: 9, + color: tokens.globalTextMuted, + letterSpacing: 0.5, + ), + maxLines: 1, + overflow: TextOverflow.clip, + ), + ), + ), + if (widget.badgeCount > 0) + Positioned( + top: 4, + left: 0, + right: 0, + child: Center( + child: Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: tokens.statusInfo, + shape: BoxShape.circle, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/widgets/widgets.dart b/app/lib/widgets/widgets.dart index 04068550..cd1c5e61 100644 --- a/app/lib/widgets/widgets.dart +++ b/app/lib/widgets/widgets.dart @@ -13,6 +13,7 @@ export 'src/clide_palette.dart'; export 'src/clide_pane_chrome.dart'; export 'src/clide_pty_view.dart'; export 'src/clide_scrollbar.dart'; +export 'src/clide_spine.dart'; export 'src/clide_surface.dart'; export 'src/clide_tab_bar.dart'; export 'src/clide_text.dart'; diff --git a/app/test/kernel/src/panels/arrangement_test.dart b/app/test/kernel/src/panels/arrangement_test.dart index 69d6f5c0..4a294d61 100644 --- a/app/test/kernel/src/panels/arrangement_test.dart +++ b/app/test/kernel/src/panels/arrangement_test.dart @@ -46,5 +46,59 @@ void main() { expect(r.definitionFor(Slots.sidebar), isNotNull); expect(r.definitionFor(Slots.statusbar), isNotNull); }); + + test('setCollapsed toggles collapsed state', () { + final a = LayoutArrangement()..applyPreset(classicPreset()); + expect(a.isCollapsed(Slots.sidebar), false); + a.setCollapsed(Slots.sidebar, true); + expect(a.isCollapsed(Slots.sidebar), true); + expect(a.isVisible(Slots.sidebar), true); + }); + + test('toggleCollapsed flips collapsed flag', () { + final a = LayoutArrangement()..applyPreset(classicPreset()); + a.toggleCollapsed(Slots.sidebar); + expect(a.isCollapsed(Slots.sidebar), true); + a.toggleCollapsed(Slots.sidebar); + expect(a.isCollapsed(Slots.sidebar), false); + }); + + test('enterFocusMode hides all slots except the focused one', () { + final a = LayoutArrangement()..applyPreset(classicPreset()); + a.enterFocusMode(Slots.workspace); + expect(a.isInFocusMode, true); + expect(a.focusModeSlot, Slots.workspace); + expect(a.isVisible(Slots.workspace), true); + expect(a.isVisible(Slots.sidebar), false); + expect(a.isVisible(Slots.contextPanel), false); + }); + + test('exitFocusMode restores prior layout', () { + final a = LayoutArrangement()..applyPreset(classicPreset()); + a.setCollapsed(Slots.sidebar, true); + a.setSize(Slots.contextPanel, 300); + a.enterFocusMode(Slots.workspace); + a.exitFocusMode(); + expect(a.isInFocusMode, false); + expect(a.isVisible(Slots.sidebar), true); + expect(a.isCollapsed(Slots.sidebar), true); + expect(a.sizeOf(Slots.contextPanel), 300); + }); + + test('toggleFocusMode enters then exits', () { + final a = LayoutArrangement()..applyPreset(classicPreset()); + a.toggleFocusMode(Slots.workspace); + expect(a.isInFocusMode, true); + a.toggleFocusMode(Slots.workspace); + expect(a.isInFocusMode, false); + expect(a.isVisible(Slots.sidebar), true); + }); + + test('applyPreset clears focus mode', () { + final a = LayoutArrangement()..applyPreset(classicPreset()); + a.enterFocusMode(Slots.workspace); + a.applyPreset(classicPreset()); + expect(a.isInFocusMode, false); + }); }); }