From 37cce1a7766e2e9cdbfabf28434e8a9151084b8b Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 28 Sep 2026 00:01:09 +0300 Subject: [PATCH] perf(shell): prevent workspace LayoutBuilder and layout rebuilds during sidebar toggle (#984) --- lib/features/main_screen/main_screen.dart | 101 ++++++++++-------- .../main_screen/sidebar_toggle_test.dart | 76 +++++++++++++ 2 files changed, 135 insertions(+), 42 deletions(-) diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index 37e4ee7..8ef270b 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -1464,6 +1464,12 @@ class _MainContentSplitState extends State<_MainContentSplit> ); } + final isAnimating = _widthSpring.isAnimating; + final remainingWidth = + math.max(0.0, constraints.maxWidth - currentW - currentHandleW); + final workspaceLaidOutWidth = + isAnimating ? constraints.maxWidth : remainingWidth; + return Row( children: [ ClipRect( @@ -1490,49 +1496,60 @@ class _MainContentSplitState extends State<_MainContentSplit> ), ), Expanded( - child: material.RepaintBoundary( - child: ValueListenableBuilder( - valueListenable: widget.workspace, - builder: (context, ws, _) { - return WorkspacePanel( - activeConnection: ws.activeConnection, - selectedRedisDb: ws.activeRedisDb, - selectedMongoDb: ws.activeMongoDB, - selectedPostgresObject: ws.selectedPostgresObject, - postgresSqlTabRequestToken: ws.postgresSqlTabRequestToken, - postgresSqlEditorContext: ws.postgresSqlEditorContext, - postgresSqlEditorContextToken: - ws.postgresSqlEditorContextToken, - selectedMysqlObject: ws.selectedMysqlObject, - mysqlSqlTabRequestToken: ws.mysqlSqlTabRequestToken, - selectedSqliteObject: ws.selectedSqliteObject, - sqliteSqlTabRequestToken: ws.sqliteSqlTabRequestToken, - selectedExtensionObject: ws.selectedExtensionObject, - lastSelectedPostgresObject: ws.lastSelectedPostgresObject, - lastSelectedMysqlObject: ws.lastSelectedMysqlObject, - lastSelectedSqliteObject: ws.lastSelectedSqliteObject, - lastSelectedExtensionObject: - ws.lastSelectedExtensionObject, - lastSelectedMongoDb: ws.lastSelectedMongoDb, - lastSelectedRedisDb: ws.lastSelectedRedisDb, - onNavigateHome: () { - widget.workspace.value = - widget.workspace.value.unselectActiveObject(); - }, - onRestoreLastSelectedObject: () { - unawaited(_restoreLastSelectedObject()); + key: const ValueKey('main_content_workspace_expanded'), + child: ClipRect( + child: OverflowBox( + alignment: Alignment.topLeft, + minWidth: workspaceLaidOutWidth, + maxWidth: workspaceLaidOutWidth, + child: material.RepaintBoundary( + child: ValueListenableBuilder( + valueListenable: widget.workspace, + builder: (context, ws, _) { + return WorkspacePanel( + activeConnection: ws.activeConnection, + selectedRedisDb: ws.activeRedisDb, + selectedMongoDb: ws.activeMongoDB, + selectedPostgresObject: ws.selectedPostgresObject, + postgresSqlTabRequestToken: + ws.postgresSqlTabRequestToken, + postgresSqlEditorContext: + ws.postgresSqlEditorContext, + postgresSqlEditorContextToken: + ws.postgresSqlEditorContextToken, + selectedMysqlObject: ws.selectedMysqlObject, + mysqlSqlTabRequestToken: ws.mysqlSqlTabRequestToken, + selectedSqliteObject: ws.selectedSqliteObject, + sqliteSqlTabRequestToken: ws.sqliteSqlTabRequestToken, + selectedExtensionObject: ws.selectedExtensionObject, + lastSelectedPostgresObject: + ws.lastSelectedPostgresObject, + lastSelectedMysqlObject: ws.lastSelectedMysqlObject, + lastSelectedSqliteObject: ws.lastSelectedSqliteObject, + lastSelectedExtensionObject: + ws.lastSelectedExtensionObject, + lastSelectedMongoDb: ws.lastSelectedMongoDb, + lastSelectedRedisDb: ws.lastSelectedRedisDb, + onNavigateHome: () { + widget.workspace.value = + widget.workspace.value.unselectActiveObject(); + }, + onRestoreLastSelectedObject: () { + unawaited(_restoreLastSelectedObject()); + }, + isReadOnly: ws.isReadOnly, + onRequestNewConnection: widget.onRequestNewConnection, + onRequestNewConnectionFromUrl: + widget.onRequestNewConnectionFromUrl, + onRequestOpenSqlite: widget.onRequestOpenSqlite, + onRequestLaunchDemo: widget.onRequestLaunchDemo, + onRequestOpenTour: widget.onRequestOpenTour, + onOpenConnection: widget.onOpenConnection, + initialMongoCollection: widget.initialMongoCollection, + ); }, - isReadOnly: ws.isReadOnly, - onRequestNewConnection: widget.onRequestNewConnection, - onRequestNewConnectionFromUrl: - widget.onRequestNewConnectionFromUrl, - onRequestOpenSqlite: widget.onRequestOpenSqlite, - onRequestLaunchDemo: widget.onRequestLaunchDemo, - onRequestOpenTour: widget.onRequestOpenTour, - onOpenConnection: widget.onOpenConnection, - initialMongoCollection: widget.initialMongoCollection, - ); - }, + ), + ), ), ), ), diff --git a/test/features/main_screen/sidebar_toggle_test.dart b/test/features/main_screen/sidebar_toggle_test.dart index 5f246b4..28637f4 100644 --- a/test/features/main_screen/sidebar_toggle_test.dart +++ b/test/features/main_screen/sidebar_toggle_test.dart @@ -11,6 +11,7 @@ import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/features/connections/connections_panel.dart'; import 'package:querya_desktop/features/main_screen/main_screen.dart'; import 'package:querya_desktop/features/main_screen/querya_window_title_bar.dart'; +import 'package:querya_desktop/features/main_screen/workspace_panel.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import '../../support/querya_theme_test_shell.dart'; @@ -250,6 +251,81 @@ void main() { findsNothing, ); }); + + testWidgets( + 'isolates workspace layout constraints during sidebar spring animation (#984)', + (tester) async { + await tester.pumpWidget( + queryaThemeTestShell( + child: const material.SizedBox( + width: 1200, + height: 800, + child: MainScreen(), + ), + ), + ); + await tester.pumpAndSettle(); + + final RenderBox mainScreenBox = + tester.renderObject(find.byType(MainScreen)); + final totalWidth = mainScreenBox.size.width; + final expectedExpandedWidth = totalWidth - 260.0 - 6.0; + + // Initially expanded: width is totalWidth - 260 - 6. + final initialWorkspaceBox = + tester.renderObject(find.byType(WorkspacePanel)); + expect( + initialWorkspaceBox.constraints.maxWidth, + closeTo(expectedExpandedWidth, 0.5), + ); + + final toggleButton = + find.byKey(const Key('title_bar_toggle_sidebar_button')); + expect(toggleButton, findsOneWidget); + + // Trigger collapse toggle. + await tester.tap(toggleButton); + await tester.pump(); + + // Intermediate animation frames while collapsing: workspace constraints + // must remain held at constraints.maxWidth, skipping per-frame re-layouts. + for (var i = 0; i < 8; i++) { + await tester.pump(const Duration(milliseconds: 16)); + final intermediateBox = + tester.renderObject(find.byType(WorkspacePanel)); + expect(intermediateBox.constraints.maxWidth, totalWidth); + } + + await tester.pumpAndSettle(); + + // Settled collapsed: full width. + final settledCollapsedBox = + tester.renderObject(find.byType(WorkspacePanel)); + expect(settledCollapsedBox.constraints.maxWidth, totalWidth); + + // Trigger expand toggle. + await tester.tap(toggleButton); + await tester.pump(); + + // Intermediate animation frames while expanding: workspace constraints + // must remain held stable at constraints.maxWidth instead of shrinking every frame. + for (var i = 0; i < 8; i++) { + await tester.pump(const Duration(milliseconds: 16)); + final intermediateBox = + tester.renderObject(find.byType(WorkspacePanel)); + expect(intermediateBox.constraints.maxWidth, totalWidth); + } + + await tester.pumpAndSettle(); + + // Settled expanded: exactly expectedExpandedWidth. + final settledExpandedBox = + tester.renderObject(find.byType(WorkspacePanel)); + expect( + settledExpandedBox.constraints.maxWidth, + closeTo(expectedExpandedWidth, 0.5), + ); + }); }); }