From 7728ee325faa5da206ac5b362e51864e9de517fe Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Wed, 28 Oct 2026 12:32:55 -0400 Subject: [PATCH] Stop the library resizing the editor's coordinates from underneath it The editor opens on top of the library, which stays mounted and rebuilds whenever something it watches changes (a cloud folder list update, a token refresh). Each rebuild replaced the shared CoordinateSystem with one sized for window height - 90, while the editor's canvas is window height - 40 since the editor took the library's 40px strip. Every item, lineup line and stroke redrawn after that landed about 6% closer to the top-left until the canvas laid out again. Only a canvas sets the coordinates now. Until one has laid out, the capture size stands in, so a strategy can still load first. --- lib/const/coordinate_system.dart | 17 +- lib/widgets/folder_navigator.dart | 4 - ...library_under_editor_coordinates_test.dart | 166 ++++++++++++++++++ 3 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 test/library_under_editor_coordinates_test.dart diff --git a/lib/const/coordinate_system.dart b/lib/const/coordinate_system.dart index 5c55da03..bc41efa2 100644 --- a/lib/const/coordinate_system.dart +++ b/lib/const/coordinate_system.dart @@ -19,7 +19,13 @@ class CoordinateSystem { isScreenshot = value; } - static CoordinateSystem? _instance; + /// The coordinates of the canvas on screen, set by that canvas as it lays + /// out. A screen without a canvas must not set them: every item, lineup + /// line and stroke on the canvas converts through them. Until a canvas has + /// laid out, the capture size stands in, so a strategy can load before its + /// canvas is on screen. + static CoordinateSystem _instance = + CoordinateSystem._(playAreaSize: screenShotSize); // Use screenshot dimensions when isScreenshot is true; otherwise use play area size Size get _effectiveSize => isScreenshot ? screenShotSize : _playAreaSize; @@ -100,7 +106,7 @@ class CoordinateSystem { factory CoordinateSystem({required Size playAreaSize}) { _instance = CoordinateSystem._(playAreaSize: playAreaSize); - return _instance!; + return _instance; } /// The coordinates every capture draws in: 1920×1080, always in screenshot @@ -133,12 +139,7 @@ class CoordinateSystem { _lastZoneIsCapture = zone[_captureZoneKey] is CoordinateSystem; } if (_lastZoneIsCapture) return _capture; - if (_instance == null) { - throw StateError( - "CoordinateSystem must be initialized with playAreaSize first", - ); - } - return _instance!; + return _instance; } Offset screenToCoordinate(Offset screenPoint) { diff --git a/lib/widgets/folder_navigator.dart b/lib/widgets/folder_navigator.dart index 9f9a8e41..b0f080e9 100644 --- a/lib/widgets/folder_navigator.dart +++ b/lib/widgets/folder_navigator.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icarus/config/platform_policy.dart'; import 'package:icarus/const/maps.dart'; -import 'package:icarus/const/coordinate_system.dart'; import 'package:icarus/const/settings.dart'; import 'package:icarus/const/update_checker.dart'; import 'package:icarus/main.dart'; @@ -203,9 +202,6 @@ class _FolderNavigatorState extends ConsumerState { final desktopUpdateController = ref.watch(desktopUpdateControllerProvider); - final double height = MediaQuery.sizeOf(context).height - 90; - final Size playAreaSize = Size(height * (16 / 9), height); - CoordinateSystem(playAreaSize: playAreaSize); final tab = ref.watch(libraryTabProvider); final workspace = ref.watch(libraryWorkspaceProvider); final isCloudWorkspace = workspace == LibraryWorkspace.cloud; diff --git a/test/library_under_editor_coordinates_test.dart b/test/library_under_editor_coordinates_test.dart new file mode 100644 index 00000000..30c67004 --- /dev/null +++ b/test/library_under_editor_coordinates_test.dart @@ -0,0 +1,166 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce_flutter/adapters.dart'; +import 'package:icarus/collab/cloud_library_models.dart'; +import 'package:icarus/config/platform_policy.dart'; +import 'package:icarus/const/coordinate_system.dart'; +import 'package:icarus/const/hive_boxes.dart'; +import 'package:icarus/const/maps.dart'; +import 'package:icarus/const/traversal_speed.dart'; +import 'package:icarus/const/update_checker.dart'; +import 'package:icarus/domain/folder.dart'; +import 'package:icarus/hive/hive_registration.dart'; +import 'package:icarus/interactive_map.dart'; +import 'package:icarus/providers/auth_provider.dart'; +import 'package:icarus/providers/collab/remote_library_provider.dart'; +import 'package:icarus/providers/desktop_update_provider.dart'; +import 'package:icarus/providers/drawing_provider.dart'; +import 'package:icarus/providers/map_provider.dart'; +import 'package:icarus/providers/pen_provider.dart'; +import 'package:icarus/providers/update_status_provider.dart'; +import 'package:icarus/providers/user_preferences_provider.dart'; +import 'package:icarus/providers/welcome_tour_provider.dart'; +import 'package:icarus/strategy/strategy_models.dart'; +import 'package:icarus/widgets/folder_navigator.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:toastification/toastification.dart'; + +class _SignedOut extends AuthProvider { + @override + AppAuthState build() => AppAuthState.fromSession(null); +} + +class _FixedMapProvider extends MapProvider { + @override + MapState build() => MapState(currentMap: MapValue.ascent, isAttack: true); +} + +class _EmptyDrawingProvider extends DrawingProvider { + @override + DrawingState build() => DrawingState(elements: const []); + + @override + void rebuildAllPaths(CoordinateSystem coordinateSystem) {} +} + +class _FixedPenProvider extends PenProvider { + @override + PenState build() => PenState( + listOfColors: const [], + color: Colors.white, + hasArrow: false, + isDotted: false, + opacity: 1, + thickness: 1, + penMode: PenMode.freeDraw, + traversalTimeEnabled: false, + activeTraversalSpeedProfile: TraversalSpeedProfile.running, + drawingCursor: null, + erasingCursor: null, + ); +} + +/// The editor's geometry: StrategyView's 40px window strip over the canvas. +class _Editor extends StatelessWidget { + const _Editor(); + + @override + Widget build(BuildContext context) => const Scaffold( + body: Column( + children: [ + SizedBox(height: 40), + Expanded(child: InteractiveMap()), + ], + ), + ); +} + +/// The editor opens on top of the library, so the library stays mounted +/// underneath and rebuilds whenever something it watches changes: a cloud +/// folder list update, a token refresh. Every placed item, lineup line and +/// stroke converts through [CoordinateSystem.instance]; a rebuild there must +/// not change the size they convert with. +void main() { + late Directory tempDir; + + setUpAll(() => registerIcarusAdapters(Hive)); + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('icarus-coordinates-'); + Hive.init(tempDir.path); + await Hive.openBox(HiveBoxNames.strategiesBox); + await Hive.openBox(HiveBoxNames.foldersBox); + await Hive.openBox(HiveBoxNames.pinnedItemsBox); + await Hive.openBox(HiveBoxNames.strategyOutboxBox); + await Hive.openBox(HiveBoxNames.cloudMediaOutboxBox); + await Hive.openBox(HiveBoxNames.mapThemeProfilesBox); + await Hive.openBox(HiveBoxNames.appPreferencesBox); + await MapThemeProfilesProvider.bootstrap(); + }); + + tearDown(() async { + await Hive.close(); + await tempDir.delete(recursive: true); + }); + + testWidgets('the library rebuilding under the editor keeps its coordinates', + (tester) async { + tester.view.physicalSize = const Size(1600, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + platformPolicyProvider.overrideWithValue(PlatformPolicy.desktop), + authProvider.overrideWith(_SignedOut.new), + appUpdateStatusProvider.overrideWith( + (ref) async => const UpdateCheckResult( + isSupported: false, + isUpdateAvailable: false, + source: 'test', + ), + ), + desktopUpdateControllerProvider.overrideWithValue(null), + welcomeTourLaunchCheckedProvider.overrideWith((ref) => true), + cloudFolderTreeProvider.overrideWith( + (_) => Stream.value(const []), + ), + cloudStrategiesProvider.overrideWith((_) => Stream.value(const [])), + mapProvider.overrideWith(_FixedMapProvider.new), + drawingProvider.overrideWith(_EmptyDrawingProvider.new), + penProvider.overrideWith(_FixedPenProvider.new), + effectiveMapThemePaletteProvider.overrideWith( + (ref) => MapThemeProfilesProvider.immutableDefaultPalette, + ), + ], + child: const ToastificationWrapper( + child: ShadApp(home: FolderNavigator()), + ), + ), + ); + await tester.pumpAndSettle(); + + final navigator = tester.state(find.byType(Navigator)); + navigator.push(MaterialPageRoute(builder: (_) => const _Editor())); + await tester.pumpAndSettle(); + + final canvas = tester.getSize(find.byType(InteractiveMap)); + final editorSize = Size(canvas.height * 16 / 9, canvas.height); + expect(CoordinateSystem.instance.playAreaSize, editorSize); + + tester + .element(find.byType(FolderNavigator, skipOffstage: false)) + .markNeedsBuild(); + await tester.pump(); + + expect(CoordinateSystem.instance.playAreaSize, editorSize); + + navigator.pop(); + await tester.pumpAndSettle(); + }); +}